vlsi commented on code in PR #6770:
URL: https://github.com/apache/jmeter/pull/6770#discussion_r4098289854
##########
src/core/src/main/java/org/apache/jmeter/threads/JMeterThread.java:
##########
@@ -526,6 +526,12 @@ private SampleResult processSampler(Sampler current,
Sampler parent, JMeterConte
&& transactionResult == null
&& transactionSampler != null
&& transactionPack != null) {
+ // Thread was stopped mid-transaction (e.g. during ramp-down).
Review Comment:
- Bug 55816 is about non-parent mode (the time after the last child sample);
it has nothing to do with this parent-mode path. Please drop that reference.
- "Ensure setTransactionDone() is called" narrates the next line. Please
state the rule instead, for example: "A transaction interrupted by a thread
stop reports the sum of its children as elapsed time; the time since the last
child ended counts as idle time."
- The issue number should not carry the meaning; the sentence has to state
the fact without it.
##########
src/core/src/main/java/org/apache/jmeter/control/TransactionController.java:
##########
@@ -252,6 +252,14 @@ public static boolean
isFromTransactionController(SampleResult res) {
public void triggerEndOfLoop() {
if(!isGenerateParentSample()) {
if (res != null) {
+ // See BUG 55816 / GitHub issue #6496
Review Comment:
This comment is incorrect: `triggerEndOfLoop()` is not called when the
thread stops. It is called from `JMeterThread.continueOnCurrentLoop`,
`breakOnCurrentLoop`, and `continueOnThreadLoop`, that is, when a failed sample
triggers "Start Next Thread Loop" or a Flow Control Action ends the loop. In
non-parent mode, a thread stop emits no transaction sample at all.
Please describe the actual case, for example: "The loop ends early, so the
time since the last child sample ended counts as idle time, as it does in
`nextWithoutTransactionSampler()`." Please also cover this path with a test
(see the review summary).
`nextWithoutTransactionSampler()` now has the same four lines. Please
extract them into a private method so the two paths cannot drift apart.
##########
src/core/src/main/java/org/apache/jmeter/threads/JMeterThread.java:
##########
@@ -526,6 +526,12 @@ private SampleResult processSampler(Sampler current,
Sampler parent, JMeterConte
&& transactionResult == null
&& transactionSampler != null
&& transactionPack != null) {
+ // Thread was stopped mid-transaction (e.g. during ramp-down).
+ // Ensure setTransactionDone() is called so that elapsed time and
idle time
+ // are correctly computed (see GitHub issue #6496 / Bug 55816).
+ if (!transactionSampler.isTransactionDone()) {
Review Comment:
This check is always true here: this branch runs only when
`transactionResult == null`, and a transaction that was already done set
`transactionResult` in the first branch of this method, unless
`doEndTransactionSampler` threw. Either drop the check, or keep it and say
which case it guards.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
Review Comment:
`setEndTime(start + elapsedTimeMs)` sets an end time in the future without
waiting for it. The child then appears to finish after the transaction's
`currentTimeInMillis()`, and `TransactionSampler.setTransactionDone()` can
compute a negative idle time. Either make the fixed elapsed time explicit with
`setStampAndTime(start, elapsedTimeMs)` and assert the resulting idle time too,
or add a comment that the test relies on this.
##########
src/core/src/main/java/org/apache/jmeter/control/TransactionSampler.java:
##########
@@ -119,7 +119,7 @@ public void addSubSamplerResult(SampleResult res) {
totalConnectTime += res.getConnectTime();
}
- protected void setTransactionDone() {
+ public void setTransactionDone() {
Review Comment:
This makes `setTransactionDone()` public API, while calling it at the wrong
moment finalizes a transaction that is still running. Please mark it
`@API(status = API.Status.INTERNAL, since = "6.0.0")` (apiguardian is already
used in `src/core`), or add a narrower public method that `JMeterThread` calls
to end an interrupted transaction.
A subclass that overrides this method as `protected` stops compiling after
this change; please mention that in `changes.xml` under incompatible changes,
or avoid widening this method.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * in non-parent mode (TransactionController with "Generate parent sample"
unchecked).
+ *
+ * <p>The test sets up a transaction with two samplers and a timer between
them.
+ * The transaction elapsed time should equal the sum of child sample
elapsed times,
+ * not be inflated by the timer delay between samples.
+ */
+ @Test
+ public void testIssue6496NonParentMode() throws Exception {
Review Comment:
This test passes on the base commit: with loop count 1 and no scheduler, the
transaction ends through the regular end-of-controller path in
`nextWithoutTransactionSampler()`, which already excluded timer delays (Bug
55816). It does not reach the changed `triggerEndOfLoop()`.
Please rename the test after the behavior rather than the ticket, for
example `aTransactionEndedByStartNextLoopExcludesTimerDelayFromElapsedTime`,
and drive it through the changed path (a failed child with "Start Next Thread
Loop", or a Flow Control Action).
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * in non-parent mode (TransactionController with "Generate parent sample"
unchecked).
+ *
+ * <p>The test sets up a transaction with two samplers and a timer between
them.
+ * The transaction elapsed time should equal the sum of child sample
elapsed times,
+ * not be inflated by the timer delay between samples.
+ */
+ @Test
+ public void testIssue6496NonParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(false);
+ transactionController.setIncludeTimers(false);
+
+ // Use a simulated elapsed time much smaller than the timer delay
+ long childElapsedMs = 10L;
+ long timerDelayMs = 200L; // timer delay before second sampler
+
+ FixedElapsedTimeSampler firstSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ firstSampler.setName("First Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Timer Before Second Sampler");
+ timer.setEnabled(true);
+
+ FixedElapsedTimeSampler secondSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ secondSampler.setName("Second Sampler");
+
+ LoopController loop = new LoopController();
+ loop.setLoops(1);
+ loop.setContinueForever(false);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ // In non-parent mode, the TransactionController fires events using
the SamplePackage
+ // of the last sampler that ran. The listener must be a child of that
sampler.
+ hashTree.add(transactionController, firstSampler);
+ hashTree.add(transactionController, secondSampler);
+ hashTree.add(secondSampler, timer);
+ hashTree.add(secondSampler, listener);
+
+ TestCompiler compiler = new TestCompiler(hashTree);
+ hashTree.traverse(compiler);
+
+ ThreadGroup threadGroup = new ThreadGroup();
+ threadGroup.setNumThreads(1);
+
+ ListenerNotifier notifier = new ListenerNotifier();
+
+ JMeterThread thread = new JMeterThread(hashTree, threadGroup,
notifier);
+ thread.setThreadGroup(threadGroup);
+ thread.run();
+
+ assertFalse(listener.getEvents().isEmpty(),
+ "At least one transaction sample should have been collected");
+
+ // Find the transaction result (not the individual sample results)
+ SampleResult transactionResult = listener.getEvents().stream()
+ .map(e -> e.getResult())
+ .filter(TransactionController::isFromTransactionController)
+ .findFirst()
+ .orElse(null);
+ assertTrue(transactionResult != null,
+ "A transaction result should have been collected");
+ // The transaction elapsed time should be the sum of child sample
times,
+ // not inflated by the timer delay between samples.
+ // Both child samples have very short simulated elapsed times (10ms
each),
+ // so the total should be well under the timer delay (200ms).
+ assertTrue(transactionResult.getTime() < timerDelayMs,
+ "Transaction elapsed time (" + transactionResult.getTime() + "
ms) should not include " +
+ "the timer delay (" + timerDelayMs + " ms)");
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * when the thread is stopped mid-transaction (e.g. during ramp-down), in
parent mode
+ * (TransactionController with "Generate parent sample" checked).
+ *
+ * <p>The test uses a scheduler end time that expires before the timer
delay completes,
+ * simulating a thread being stopped mid-transaction during ramp-down. The
transaction
+ * elapsed time should equal the child sample elapsed time, not be
inflated by the timer.
+ */
+ @Test
+ public void testIssue6496ParentMode() throws Exception {
Review Comment:
This test passes on the base commit and does not reproduce the scenario from
#6496. The 5000 ms timer runs before the only sampler,
`TimerService.adjustDelay` returns `-1` at once, and `running` becomes `false`
before any child runs. The transaction has zero children and an elapsed time of
0 with or without the fix.
The bug needs at least one child that ran after a delay, and the stop has to
happen on the delay before the next child: for example a 500 ms timer on the
first sampler, a 5000 ms timer on the second one, and `setEndTime(now + 1500)`.
The expected elapsed time is then the first child's time, and the idle time
holds the 500 ms delay.
Please also rename the test after the behavior, for example
`aTransactionInterruptedByTheSchedulerExcludesTimerDelayFromElapsedTime`.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
Review Comment:
Please open the Javadoc with the rule the test guards, then state the old
defect in one past-tense sentence, for example: "A transaction whose thread
stops before the next child sample reports the sum of its children as elapsed
time. The timer delays before its children used to be counted in the elapsed
time (#6496)." "Test for GitHub issue #6496" says nothing once the number is
covered.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * in non-parent mode (TransactionController with "Generate parent sample"
unchecked).
+ *
+ * <p>The test sets up a transaction with two samplers and a timer between
them.
+ * The transaction elapsed time should equal the sum of child sample
elapsed times,
+ * not be inflated by the timer delay between samples.
+ */
+ @Test
+ public void testIssue6496NonParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(false);
+ transactionController.setIncludeTimers(false);
+
+ // Use a simulated elapsed time much smaller than the timer delay
+ long childElapsedMs = 10L;
+ long timerDelayMs = 200L; // timer delay before second sampler
+
+ FixedElapsedTimeSampler firstSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ firstSampler.setName("First Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Timer Before Second Sampler");
+ timer.setEnabled(true);
+
+ FixedElapsedTimeSampler secondSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ secondSampler.setName("Second Sampler");
+
+ LoopController loop = new LoopController();
+ loop.setLoops(1);
+ loop.setContinueForever(false);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ // In non-parent mode, the TransactionController fires events using
the SamplePackage
+ // of the last sampler that ran. The listener must be a child of that
sampler.
+ hashTree.add(transactionController, firstSampler);
+ hashTree.add(transactionController, secondSampler);
+ hashTree.add(secondSampler, timer);
+ hashTree.add(secondSampler, listener);
+
+ TestCompiler compiler = new TestCompiler(hashTree);
+ hashTree.traverse(compiler);
+
+ ThreadGroup threadGroup = new ThreadGroup();
+ threadGroup.setNumThreads(1);
+
+ ListenerNotifier notifier = new ListenerNotifier();
+
+ JMeterThread thread = new JMeterThread(hashTree, threadGroup,
notifier);
+ thread.setThreadGroup(threadGroup);
+ thread.run();
+
+ assertFalse(listener.getEvents().isEmpty(),
+ "At least one transaction sample should have been collected");
+
+ // Find the transaction result (not the individual sample results)
+ SampleResult transactionResult = listener.getEvents().stream()
+ .map(e -> e.getResult())
+ .filter(TransactionController::isFromTransactionController)
+ .findFirst()
+ .orElse(null);
+ assertTrue(transactionResult != null,
Review Comment:
`assertTrue(x != null)` prints `expected: <true> but was: <false>`. Please
use `assertNotNull`, or better, drop the `orElse(null)` and let `orElseThrow()`
fail with a message that says which events were collected.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * in non-parent mode (TransactionController with "Generate parent sample"
unchecked).
+ *
+ * <p>The test sets up a transaction with two samplers and a timer between
them.
+ * The transaction elapsed time should equal the sum of child sample
elapsed times,
+ * not be inflated by the timer delay between samples.
+ */
+ @Test
+ public void testIssue6496NonParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(false);
+ transactionController.setIncludeTimers(false);
+
+ // Use a simulated elapsed time much smaller than the timer delay
+ long childElapsedMs = 10L;
+ long timerDelayMs = 200L; // timer delay before second sampler
+
+ FixedElapsedTimeSampler firstSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ firstSampler.setName("First Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Timer Before Second Sampler");
+ timer.setEnabled(true);
+
+ FixedElapsedTimeSampler secondSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ secondSampler.setName("Second Sampler");
+
+ LoopController loop = new LoopController();
+ loop.setLoops(1);
+ loop.setContinueForever(false);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ // In non-parent mode, the TransactionController fires events using
the SamplePackage
+ // of the last sampler that ran. The listener must be a child of that
sampler.
Review Comment:
The listener does not need to be a child of the sampler. What happens here
is that `hashTree.add(secondSampler, timer)` (`add(key).add(value)`) adds
`secondSampler` as a new top-level node, and the timer and listener hang under
that copy rather than under the sampler inside the Transaction Controller.
Please build the tree through the returned subtrees: `HashTree tcTree =
hashTree.add(loop).add(transactionController); tcTree.add(firstSampler);
tcTree.add(secondSampler).add(timer); tcTree.add(listener);`, and drop this
comment.
##########
.github/workflows/gradle-wrapper-validation.yml:
##########
@@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- - uses:
gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c #
v5.0.2
+ - uses:
gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 #
v6.2.0
Review Comment:
Unrelated to #6496. Please move this to its own PR.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * in non-parent mode (TransactionController with "Generate parent sample"
unchecked).
+ *
+ * <p>The test sets up a transaction with two samplers and a timer between
them.
+ * The transaction elapsed time should equal the sum of child sample
elapsed times,
+ * not be inflated by the timer delay between samples.
+ */
+ @Test
+ public void testIssue6496NonParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(false);
+ transactionController.setIncludeTimers(false);
+
+ // Use a simulated elapsed time much smaller than the timer delay
+ long childElapsedMs = 10L;
+ long timerDelayMs = 200L; // timer delay before second sampler
+
+ FixedElapsedTimeSampler firstSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ firstSampler.setName("First Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Timer Before Second Sampler");
+ timer.setEnabled(true);
+
+ FixedElapsedTimeSampler secondSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ secondSampler.setName("Second Sampler");
+
+ LoopController loop = new LoopController();
+ loop.setLoops(1);
+ loop.setContinueForever(false);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ // In non-parent mode, the TransactionController fires events using
the SamplePackage
+ // of the last sampler that ran. The listener must be a child of that
sampler.
+ hashTree.add(transactionController, firstSampler);
+ hashTree.add(transactionController, secondSampler);
+ hashTree.add(secondSampler, timer);
+ hashTree.add(secondSampler, listener);
+
+ TestCompiler compiler = new TestCompiler(hashTree);
+ hashTree.traverse(compiler);
+
+ ThreadGroup threadGroup = new ThreadGroup();
+ threadGroup.setNumThreads(1);
+
+ ListenerNotifier notifier = new ListenerNotifier();
+
+ JMeterThread thread = new JMeterThread(hashTree, threadGroup,
notifier);
+ thread.setThreadGroup(threadGroup);
+ thread.run();
+
+ assertFalse(listener.getEvents().isEmpty(),
+ "At least one transaction sample should have been collected");
+
+ // Find the transaction result (not the individual sample results)
+ SampleResult transactionResult = listener.getEvents().stream()
+ .map(e -> e.getResult())
+ .filter(TransactionController::isFromTransactionController)
+ .findFirst()
+ .orElse(null);
+ assertTrue(transactionResult != null,
+ "A transaction result should have been collected");
+ // The transaction elapsed time should be the sum of child sample
times,
+ // not inflated by the timer delay between samples.
+ // Both child samples have very short simulated elapsed times (10ms
each),
+ // so the total should be well under the timer delay (200ms).
+ assertTrue(transactionResult.getTime() < timerDelayMs,
+ "Transaction elapsed time (" + transactionResult.getTime() + "
ms) should not include " +
+ "the timer delay (" + timerDelayMs + " ms)");
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * when the thread is stopped mid-transaction (e.g. during ramp-down), in
parent mode
+ * (TransactionController with "Generate parent sample" checked).
+ *
+ * <p>The test uses a scheduler end time that expires before the timer
delay completes,
+ * simulating a thread being stopped mid-transaction during ramp-down. The
transaction
+ * elapsed time should equal the child sample elapsed time, not be
inflated by the timer.
+ */
+ @Test
+ public void testIssue6496ParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(true);
+ transactionController.setIncludeTimers(false);
+
+ long childElapsedMs = 10L;
+ long timerDelayMs = 5000L; // long timer that will be cut short by
scheduler
+
+ FixedElapsedTimeSampler sampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ sampler.setName("Child Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Long Timer");
+ timer.setEnabled(true);
+
+ LoopController loop = new LoopController();
+ loop.setLoops(LoopController.INFINITE_LOOP_COUNT);
+ loop.setContinueForever(true);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ hashTree.add(transactionController, listener);
+ hashTree.add(transactionController, sampler);
+ hashTree.add(sampler, timer);
+
+ TestCompiler compiler = new TestCompiler(hashTree);
+ hashTree.traverse(compiler);
+
+ ThreadGroup threadGroup = new ThreadGroup();
+ threadGroup.setNumThreads(1);
+
+ ListenerNotifier notifier = new ListenerNotifier();
+
+ // Use scheduler to stop the thread after a short time (before the
timer delay completes).
+ // The scheduler end time is set to expire before the timer delay, so
adjustDelay() returns
+ // -1 and running is set to false immediately, simulating a thread
stopped mid-transaction.
+ long maxDuration = 200L;
+ JMeterThread thread = new JMeterThread(hashTree, threadGroup,
notifier);
+ thread.setScheduled(true);
+ thread.setEndTime(System.currentTimeMillis() + maxDuration);
+ thread.setThreadGroup(threadGroup);
+ thread.run();
+
+ assertFalse(listener.getEvents().isEmpty(),
+ "At least one transaction sample should have been collected");
+
+ // The last transaction event is the one interrupted during ramp-down.
+ // Its elapsed time should be close to the child sample elapsed time,
+ // not inflated by the long timer delay.
+ SampleResult lastTransaction =
listener.getEvents().get(listener.getEvents().size() - 1).getResult();
Review Comment:
`CollectSamplesListener` keeps a reference to the `SampleResult`, and after
the thread stops, `JMeterThread.run()` still calls
`threadGroupLoopController.next()`, which calls `setTransactionDone()` on this
same object. The values read here are therefore the ones computed after
notification, not the ones a `ResultCollector` writes to the file. Please
record `getTime()`, `getIdleTime()`, and `getResponseMessage()` inside
`sampleOccurred` and assert on that snapshot.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * in non-parent mode (TransactionController with "Generate parent sample"
unchecked).
+ *
+ * <p>The test sets up a transaction with two samplers and a timer between
them.
+ * The transaction elapsed time should equal the sum of child sample
elapsed times,
+ * not be inflated by the timer delay between samples.
+ */
+ @Test
+ public void testIssue6496NonParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(false);
+ transactionController.setIncludeTimers(false);
+
+ // Use a simulated elapsed time much smaller than the timer delay
+ long childElapsedMs = 10L;
+ long timerDelayMs = 200L; // timer delay before second sampler
+
+ FixedElapsedTimeSampler firstSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ firstSampler.setName("First Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Timer Before Second Sampler");
+ timer.setEnabled(true);
+
+ FixedElapsedTimeSampler secondSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ secondSampler.setName("Second Sampler");
+
+ LoopController loop = new LoopController();
+ loop.setLoops(1);
+ loop.setContinueForever(false);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ // In non-parent mode, the TransactionController fires events using
the SamplePackage
+ // of the last sampler that ran. The listener must be a child of that
sampler.
+ hashTree.add(transactionController, firstSampler);
+ hashTree.add(transactionController, secondSampler);
+ hashTree.add(secondSampler, timer);
+ hashTree.add(secondSampler, listener);
+
+ TestCompiler compiler = new TestCompiler(hashTree);
+ hashTree.traverse(compiler);
+
+ ThreadGroup threadGroup = new ThreadGroup();
+ threadGroup.setNumThreads(1);
+
+ ListenerNotifier notifier = new ListenerNotifier();
+
+ JMeterThread thread = new JMeterThread(hashTree, threadGroup,
notifier);
+ thread.setThreadGroup(threadGroup);
+ thread.run();
+
+ assertFalse(listener.getEvents().isEmpty(),
+ "At least one transaction sample should have been collected");
+
+ // Find the transaction result (not the individual sample results)
+ SampleResult transactionResult = listener.getEvents().stream()
+ .map(e -> e.getResult())
+ .filter(TransactionController::isFromTransactionController)
+ .findFirst()
+ .orElse(null);
+ assertTrue(transactionResult != null,
+ "A transaction result should have been collected");
+ // The transaction elapsed time should be the sum of child sample
times,
+ // not inflated by the timer delay between samples.
+ // Both child samples have very short simulated elapsed times (10ms
each),
+ // so the total should be well under the timer delay (200ms).
+ assertTrue(transactionResult.getTime() < timerDelayMs,
+ "Transaction elapsed time (" + transactionResult.getTime() + "
ms) should not include " +
+ "the timer delay (" + timerDelayMs + " ms)");
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * when the thread is stopped mid-transaction (e.g. during ramp-down), in
parent mode
+ * (TransactionController with "Generate parent sample" checked).
+ *
+ * <p>The test uses a scheduler end time that expires before the timer
delay completes,
+ * simulating a thread being stopped mid-transaction during ramp-down. The
transaction
+ * elapsed time should equal the child sample elapsed time, not be
inflated by the timer.
+ */
+ @Test
+ public void testIssue6496ParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(true);
+ transactionController.setIncludeTimers(false);
+
+ long childElapsedMs = 10L;
+ long timerDelayMs = 5000L; // long timer that will be cut short by
scheduler
+
+ FixedElapsedTimeSampler sampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ sampler.setName("Child Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Long Timer");
+ timer.setEnabled(true);
+
+ LoopController loop = new LoopController();
+ loop.setLoops(LoopController.INFINITE_LOOP_COUNT);
+ loop.setContinueForever(true);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ hashTree.add(transactionController, listener);
+ hashTree.add(transactionController, sampler);
+ hashTree.add(sampler, timer);
Review Comment:
Same tree-building issue as in the other test: `hashTree.add(sampler,
timer)` creates a top-level copy of `sampler`. Please use
`tcTree.add(sampler).add(timer)`.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * in non-parent mode (TransactionController with "Generate parent sample"
unchecked).
+ *
+ * <p>The test sets up a transaction with two samplers and a timer between
them.
+ * The transaction elapsed time should equal the sum of child sample
elapsed times,
+ * not be inflated by the timer delay between samples.
+ */
+ @Test
+ public void testIssue6496NonParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(false);
+ transactionController.setIncludeTimers(false);
+
+ // Use a simulated elapsed time much smaller than the timer delay
+ long childElapsedMs = 10L;
+ long timerDelayMs = 200L; // timer delay before second sampler
+
+ FixedElapsedTimeSampler firstSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ firstSampler.setName("First Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Timer Before Second Sampler");
+ timer.setEnabled(true);
+
+ FixedElapsedTimeSampler secondSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ secondSampler.setName("Second Sampler");
+
+ LoopController loop = new LoopController();
+ loop.setLoops(1);
+ loop.setContinueForever(false);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ // In non-parent mode, the TransactionController fires events using
the SamplePackage
+ // of the last sampler that ran. The listener must be a child of that
sampler.
+ hashTree.add(transactionController, firstSampler);
+ hashTree.add(transactionController, secondSampler);
+ hashTree.add(secondSampler, timer);
+ hashTree.add(secondSampler, listener);
+
+ TestCompiler compiler = new TestCompiler(hashTree);
+ hashTree.traverse(compiler);
+
+ ThreadGroup threadGroup = new ThreadGroup();
+ threadGroup.setNumThreads(1);
+
+ ListenerNotifier notifier = new ListenerNotifier();
+
+ JMeterThread thread = new JMeterThread(hashTree, threadGroup,
notifier);
+ thread.setThreadGroup(threadGroup);
+ thread.run();
+
+ assertFalse(listener.getEvents().isEmpty(),
+ "At least one transaction sample should have been collected");
+
+ // Find the transaction result (not the individual sample results)
+ SampleResult transactionResult = listener.getEvents().stream()
+ .map(e -> e.getResult())
+ .filter(TransactionController::isFromTransactionController)
+ .findFirst()
+ .orElse(null);
+ assertTrue(transactionResult != null,
+ "A transaction result should have been collected");
+ // The transaction elapsed time should be the sum of child sample
times,
+ // not inflated by the timer delay between samples.
+ // Both child samples have very short simulated elapsed times (10ms
each),
+ // so the total should be well under the timer delay (200ms).
+ assertTrue(transactionResult.getTime() < timerDelayMs,
+ "Transaction elapsed time (" + transactionResult.getTime() + "
ms) should not include " +
+ "the timer delay (" + timerDelayMs + " ms)");
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * when the thread is stopped mid-transaction (e.g. during ramp-down), in
parent mode
+ * (TransactionController with "Generate parent sample" checked).
+ *
+ * <p>The test uses a scheduler end time that expires before the timer
delay completes,
+ * simulating a thread being stopped mid-transaction during ramp-down. The
transaction
+ * elapsed time should equal the child sample elapsed time, not be
inflated by the timer.
+ */
+ @Test
+ public void testIssue6496ParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(true);
+ transactionController.setIncludeTimers(false);
+
+ long childElapsedMs = 10L;
+ long timerDelayMs = 5000L; // long timer that will be cut short by
scheduler
+
+ FixedElapsedTimeSampler sampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ sampler.setName("Child Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Long Timer");
+ timer.setEnabled(true);
+
+ LoopController loop = new LoopController();
+ loop.setLoops(LoopController.INFINITE_LOOP_COUNT);
+ loop.setContinueForever(true);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ hashTree.add(transactionController, listener);
+ hashTree.add(transactionController, sampler);
+ hashTree.add(sampler, timer);
+
+ TestCompiler compiler = new TestCompiler(hashTree);
+ hashTree.traverse(compiler);
+
+ ThreadGroup threadGroup = new ThreadGroup();
+ threadGroup.setNumThreads(1);
+
+ ListenerNotifier notifier = new ListenerNotifier();
+
+ // Use scheduler to stop the thread after a short time (before the
timer delay completes).
+ // The scheduler end time is set to expire before the timer delay, so
adjustDelay() returns
+ // -1 and running is set to false immediately, simulating a thread
stopped mid-transaction.
+ long maxDuration = 200L;
+ JMeterThread thread = new JMeterThread(hashTree, threadGroup,
notifier);
+ thread.setScheduled(true);
+ thread.setEndTime(System.currentTimeMillis() + maxDuration);
+ thread.setThreadGroup(threadGroup);
+ thread.run();
+
+ assertFalse(listener.getEvents().isEmpty(),
+ "At least one transaction sample should have been collected");
+
+ // The last transaction event is the one interrupted during ramp-down.
+ // Its elapsed time should be close to the child sample elapsed time,
+ // not inflated by the long timer delay.
+ SampleResult lastTransaction =
listener.getEvents().get(listener.getEvents().size() - 1).getResult();
+
assertTrue(TransactionController.isFromTransactionController(lastTransaction),
+ "Result should be from TransactionController");
+ assertTrue(lastTransaction.getTime() < timerDelayMs,
Review Comment:
Same as in the non-parent test: please assert the exact expected elapsed
time and idle time with `assertEquals` instead of `< timerDelayMs`. The
`assertTrue(isFromTransactionController(...))` above prints nothing useful on
failure either; asserting the response message with `assertEquals` shows what
was received.
##########
src/components/src/test/java/org/apache/jmeter/control/TestTransactionController.java:
##########
@@ -18,23 +18,224 @@
package org.apache.jmeter.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.junit.JMeterTestCase;
import org.apache.jmeter.sampler.DebugSampler;
+import org.apache.jmeter.samplers.AbstractSampler;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.test.samplers.CollectSamplesListener;
+import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterThread;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.threads.ListenerNotifier;
import org.apache.jmeter.threads.TestCompiler;
import org.apache.jmeter.threads.ThreadGroup;
+import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.ListedHashTree;
import org.junit.jupiter.api.Test;
public class TestTransactionController extends JMeterTestCase {
+ /**
+ * A simple sampler that returns a successful result with a fixed elapsed
time.
+ */
+ private static class FixedElapsedTimeSampler extends AbstractSampler {
+ private static final long serialVersionUID = 1L;
+ private final long elapsedTimeMs;
+
+ FixedElapsedTimeSampler(long elapsedTimeMs) {
+ this.elapsedTimeMs = elapsedTimeMs;
+ }
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult result = new SampleResult();
+ result.setSampleLabel(getName());
+ result.sampleStart();
+ result.setSuccessful(true);
+ result.setResponseCodeOK();
+ // Simulate elapsed time by setting start/end times directly
+ long start = result.getStartTime();
+ result.setEndTime(start + elapsedTimeMs);
+ return result;
+ }
+ }
+
+ /**
+ * A timer that returns a fixed delay.
+ */
+ private static class FixedDelayTimer extends AbstractTestElement
implements Timer {
+ private static final long serialVersionUID = 1L;
+ private final long delayMs;
+
+ FixedDelayTimer(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public long delay() {
+ return delayMs;
+ }
+ }
+
+ /**
+ * Test for GitHub issue #6496: transaction elapsed time should not
include timer delay
+ * in non-parent mode (TransactionController with "Generate parent sample"
unchecked).
+ *
+ * <p>The test sets up a transaction with two samplers and a timer between
them.
+ * The transaction elapsed time should equal the sum of child sample
elapsed times,
+ * not be inflated by the timer delay between samples.
+ */
+ @Test
+ public void testIssue6496NonParentMode() throws Exception {
+ JMeterContextService.getContext().setVariables(new JMeterVariables());
+
+ CollectSamplesListener listener = new CollectSamplesListener();
+
+ TransactionController transactionController = new
TransactionController();
+ transactionController.setGenerateParentSample(false);
+ transactionController.setIncludeTimers(false);
+
+ // Use a simulated elapsed time much smaller than the timer delay
+ long childElapsedMs = 10L;
+ long timerDelayMs = 200L; // timer delay before second sampler
+
+ FixedElapsedTimeSampler firstSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ firstSampler.setName("First Sampler");
+
+ FixedDelayTimer timer = new FixedDelayTimer(timerDelayMs);
+ timer.setName("Timer Before Second Sampler");
+ timer.setEnabled(true);
+
+ FixedElapsedTimeSampler secondSampler = new
FixedElapsedTimeSampler(childElapsedMs);
+ secondSampler.setName("Second Sampler");
+
+ LoopController loop = new LoopController();
+ loop.setLoops(1);
+ loop.setContinueForever(false);
+ loop.setEnabled(true);
+
+ ListedHashTree hashTree = new ListedHashTree();
+ hashTree.add(loop);
+ hashTree.add(loop, transactionController);
+ // In non-parent mode, the TransactionController fires events using
the SamplePackage
+ // of the last sampler that ran. The listener must be a child of that
sampler.
+ hashTree.add(transactionController, firstSampler);
+ hashTree.add(transactionController, secondSampler);
+ hashTree.add(secondSampler, timer);
+ hashTree.add(secondSampler, listener);
+
+ TestCompiler compiler = new TestCompiler(hashTree);
+ hashTree.traverse(compiler);
+
+ ThreadGroup threadGroup = new ThreadGroup();
+ threadGroup.setNumThreads(1);
+
+ ListenerNotifier notifier = new ListenerNotifier();
+
+ JMeterThread thread = new JMeterThread(hashTree, threadGroup,
notifier);
+ thread.setThreadGroup(threadGroup);
+ thread.run();
+
+ assertFalse(listener.getEvents().isEmpty(),
+ "At least one transaction sample should have been collected");
+
+ // Find the transaction result (not the individual sample results)
+ SampleResult transactionResult = listener.getEvents().stream()
+ .map(e -> e.getResult())
+ .filter(TransactionController::isFromTransactionController)
+ .findFirst()
+ .orElse(null);
+ assertTrue(transactionResult != null,
+ "A transaction result should have been collected");
+ // The transaction elapsed time should be the sum of child sample
times,
+ // not inflated by the timer delay between samples.
+ // Both child samples have very short simulated elapsed times (10ms
each),
+ // so the total should be well under the timer delay (200ms).
+ assertTrue(transactionResult.getTime() < timerDelayMs,
Review Comment:
`time < timerDelayMs` would still pass if the transaction included half the
timer. The children have fixed elapsed times, so the expected value is known:
please use `assertEquals(2 * childElapsedMs, transactionResult.getTime(), ...)`
(and assert the idle time as well, since the fix moves the delay there). The
value must be captured inside the listener's `sampleOccurred`, because the
`SampleResult` is mutated after notification (see the review summary).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]