diff --git a/src/main/java/com/blazemeter/jmeter/http2/control/HTTP2Controller.java b/src/main/java/com/blazemeter/jmeter/http2/control/HTTP2Controller.java index 9d4b557..98412f5 100644 --- a/src/main/java/com/blazemeter/jmeter/http2/control/HTTP2Controller.java +++ b/src/main/java/com/blazemeter/jmeter/http2/control/HTTP2Controller.java @@ -368,14 +368,13 @@ private void putSamplerPackageInCompilerMap(Object sampler, SamplePackage pack) } /** - * Parent synthetic sampler must not inherit child assertions/post-processors: JMeter would run - * them against the aggregated parent {@link SampleResult}, whose response body is empty. + * Parent synthetic sampler must not inherit child timers, assertions, or post-processors. */ private static SamplePackage createParentExecutionPackage(SamplePackage childPack) { SamplePackage parentPack = new SamplePackage( childPack.getConfigs(), childPack.getSampleListeners(), - childPack.getTimers(), + new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), diff --git a/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java b/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java index 529c270..20230fe 100644 --- a/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java +++ b/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java @@ -49,6 +49,7 @@ import org.apache.jmeter.threads.JMeterVariables; import org.apache.jmeter.threads.SamplePackage; import org.apache.jmeter.threads.TestCompiler; +import org.apache.jmeter.timers.Timer; import org.apache.jmeter.util.JMeterUtils; import org.apache.jorphan.util.JOrphanUtils; import org.apache.oro.text.MalformedCachePatternException; @@ -152,6 +153,7 @@ protected Map childValue( private int requestTimeout; private HTTPSampleResult result; private transient List suppressedPreProcessors; + private transient List suppressedTimers; private transient SamplePackage suppressedSamplePackage; private transient boolean profileInferenceWarningLogged; private transient boolean asyncParentSampleEnabled; @@ -529,8 +531,8 @@ protected HTTPSampleResult sample(URL url, String method, boolean areFollowingRe /** * When {@link #isAsyncParentSampleEnabled()} and this sampler returns {@code null} to JMeter, * run post-processors and assertions here (same order as {@code JMeterThread}) so child elements - * still apply; pre-processors already ran on the initial async dispatch and stay suppressed until - * {@link #restoreSuppressedPreProcessors()} in {@code finally}. + * still apply; pre-processors and timers already ran on the initial async dispatch and stay + * suppressed until {@link #restoreSuppressedPreProcessors()} in {@code finally}. */ private void applyAsyncParentCompletionPipeline(HTTPSampleResult res) { if (res == null || !isAsyncParentSampleEnabled()) { @@ -1083,8 +1085,12 @@ public void iterationStart(LoopIterationEvent iterEvent) { } } + /** + * Clears pre-processors and timers from the {@link SamplePackage} before the async completion + * pass so JMeter does not run them twice. + */ public void suppressPreProcessorsOnce() { - if (suppressedPreProcessors != null) { + if (suppressedSamplePackage != null) { return; } try { @@ -1099,21 +1105,32 @@ public void suppressPreProcessorsOnce() { return; } SamplePackage pack = getSamplePackageFromCompiler(compiler); - if (pack == null || pack.getPreProcessors() == null) { - LOG.debug("No SamplePackage found for sampler={}, skipping pre-processor suppression", + if (pack == null) { + LOG.debug( + "No SamplePackage found for sampler={}, skipping async completion suppression", getName()); return; } + boolean suppressed = false; List currentPre = pack.getPreProcessors(); - if (currentPre.isEmpty()) { - return; + if (currentPre != null && !currentPre.isEmpty()) { + suppressedPreProcessors = new ArrayList<>(currentPre); + currentPre.clear(); + suppressed = true; + } + List currentTimers = pack.getTimers(); + if (currentTimers != null && !currentTimers.isEmpty()) { + suppressedTimers = new ArrayList<>(currentTimers); + currentTimers.clear(); + suppressed = true; + } + if (suppressed) { + suppressedSamplePackage = pack; + LOG.debug("Pre-processors/timers suppressed for async completion run (sampler={})", + getName()); } - suppressedPreProcessors = new ArrayList<>(currentPre); - suppressedSamplePackage = pack; - currentPre.clear(); - LOG.debug("Pre-processors suppressed for async completion run (sampler={})", getName()); } catch (Exception e) { - LOG.debug("Failed to suppress pre-processors for async completion", e); + LOG.debug("Failed to suppress pre-processors/timers for async completion", e); } } @@ -1130,18 +1147,28 @@ private SamplePackage getSamplePackageFromCompiler(TestCompiler compiler) { } private void restoreSuppressedPreProcessors() { - if (suppressedPreProcessors == null || suppressedSamplePackage == null) { + if (suppressedSamplePackage == null) { return; } try { - List currentPre = suppressedSamplePackage.getPreProcessors(); - if (currentPre != null) { - currentPre.clear(); - currentPre.addAll(suppressedPreProcessors); + if (suppressedPreProcessors != null) { + List currentPre = suppressedSamplePackage.getPreProcessors(); + if (currentPre != null) { + currentPre.clear(); + currentPre.addAll(suppressedPreProcessors); + } + } + if (suppressedTimers != null) { + List currentTimers = suppressedSamplePackage.getTimers(); + if (currentTimers != null) { + currentTimers.clear(); + currentTimers.addAll(suppressedTimers); + } } - LOG.debug("Pre-processors restored after async completion (sampler={})", getName()); + LOG.debug("Pre-processors/timers restored after async completion (sampler={})", getName()); } finally { suppressedPreProcessors = null; + suppressedTimers = null; suppressedSamplePackage = null; } } diff --git a/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2AsyncParentSampleAssertionTest.java b/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2AsyncParentSampleAssertionTest.java index 07998cf..a484aeb 100644 --- a/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2AsyncParentSampleAssertionTest.java +++ b/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2AsyncParentSampleAssertionTest.java @@ -212,6 +212,7 @@ public void parentSampleExecutionDoesNotReRunChildAssertionsOnEmptyParentBody() SamplePackage parentPack = getCompiler(jmeterThread).configureSampler(parentSampler); assertThat(parentPack.getAssertions()).isEmpty(); + assertThat(parentPack.getTimers()).isEmpty(); SampleResult parentResult = jmeterContext.getPreviousResult(); assertThat(parentResult).isNotNull(); diff --git a/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerAsyncTimerTest.java b/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerAsyncTimerTest.java new file mode 100644 index 0000000..6ba2b36 --- /dev/null +++ b/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerAsyncTimerTest.java @@ -0,0 +1,142 @@ +package com.blazemeter.jmeter.http2.sampler; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import org.apache.jmeter.control.LoopController; +import org.apache.jmeter.processor.PreProcessor; +import org.apache.jmeter.testelement.AbstractTestElement; +import org.apache.jmeter.timers.ConstantTimer; +import org.apache.jmeter.threads.JMeterContextService; +import org.apache.jmeter.threads.JMeterThread; +import org.apache.jmeter.threads.JMeterThreadMonitor; +import org.apache.jmeter.threads.ListenerNotifier; +import org.apache.jmeter.threads.SamplePackage; +import org.apache.jmeter.threads.TestCompiler; +import org.apache.jorphan.collections.HashTree; +import org.junit.Test; + +public class HTTP2SamplerAsyncTimerTest { + + private static class NoOpPreProcessor extends AbstractTestElement implements PreProcessor { + @Override + public void process() { + // no-op + } + } + + @Test + public void shouldSuppressAndRestoreTimersForAsyncCompletion() throws Exception { + JMeterTestUtils.setupJmeterEnv(); + + HTTP2Sampler sampler = new HTTP2Sampler(); + ConstantTimer timer = new ConstantTimer(); + timer.setDelay("100"); + + HashTree testTree = new HashTree(); + LoopController controller = new LoopController(); + controller.setLoops(1); + controller.initialize(); + HashTree controllerTree = testTree.add(controller); + controllerTree.add(sampler); + controllerTree.add(sampler, timer); + + JMeterThreadMonitor monitor = thread -> { + // no-op + }; + JMeterThread thread = new JMeterThread(testTree, monitor, new ListenerNotifier()); + JMeterContextService.getContext().setThread(thread); + + TestCompiler compiler = getCompiler(thread); + testTree.traverse(compiler); + SamplePackage samplePackage = compiler.configureSampler(sampler); + assertThat(samplePackage.getTimers()).hasSize(1); + + sampler.suppressPreProcessorsOnce(); + assertThat(samplePackage.getTimers()).isEmpty(); + + restoreSuppressedPreProcessors(sampler); + assertThat(samplePackage.getTimers()).hasSize(1); + } + + @Test + public void shouldSuppressTimersWhenNoPreProcessors() throws Exception { + JMeterTestUtils.setupJmeterEnv(); + + HTTP2Sampler sampler = new HTTP2Sampler(); + ConstantTimer timer = new ConstantTimer(); + timer.setDelay("50"); + + HashTree testTree = new HashTree(); + LoopController controller = new LoopController(); + controller.setLoops(1); + controller.initialize(); + HashTree controllerTree = testTree.add(controller); + controllerTree.add(sampler); + controllerTree.add(sampler, timer); + + JMeterThread thread = new JMeterThread(testTree, thread1 -> { + }, new ListenerNotifier()); + JMeterContextService.getContext().setThread(thread); + + TestCompiler compiler = getCompiler(thread); + testTree.traverse(compiler); + SamplePackage samplePackage = compiler.configureSampler(sampler); + assertThat(samplePackage.getPreProcessors()).isEmpty(); + assertThat(samplePackage.getTimers()).hasSize(1); + + sampler.suppressPreProcessorsOnce(); + assertThat(samplePackage.getTimers()).isEmpty(); + + restoreSuppressedPreProcessors(sampler); + assertThat(samplePackage.getTimers()).hasSize(1); + } + + @Test + public void shouldSuppressBothPreProcessorsAndTimers() throws Exception { + JMeterTestUtils.setupJmeterEnv(); + + HTTP2Sampler sampler = new HTTP2Sampler(); + ConstantTimer timer = new ConstantTimer(); + timer.setDelay("100"); + PreProcessor preProcessor = new NoOpPreProcessor(); + + HashTree testTree = new HashTree(); + LoopController controller = new LoopController(); + controller.setLoops(1); + controller.initialize(); + HashTree controllerTree = testTree.add(controller); + controllerTree.add(sampler); + controllerTree.add(sampler, timer); + controllerTree.add(sampler, preProcessor); + + JMeterThread thread = new JMeterThread(testTree, thread1 -> { + }, new ListenerNotifier()); + JMeterContextService.getContext().setThread(thread); + + TestCompiler compiler = getCompiler(thread); + testTree.traverse(compiler); + SamplePackage samplePackage = compiler.configureSampler(sampler); + + sampler.suppressPreProcessorsOnce(); + assertThat(samplePackage.getPreProcessors()).isEmpty(); + assertThat(samplePackage.getTimers()).isEmpty(); + + restoreSuppressedPreProcessors(sampler); + assertThat(samplePackage.getPreProcessors()).hasSize(1); + assertThat(samplePackage.getTimers()).hasSize(1); + } + + private TestCompiler getCompiler(JMeterThread thread) throws Exception { + Field compilerField = JMeterThread.class.getDeclaredField("compiler"); + compilerField.setAccessible(true); + return (TestCompiler) compilerField.get(thread); + } + + private void restoreSuppressedPreProcessors(HTTP2Sampler sampler) throws Exception { + Method restoreMethod = HTTP2Sampler.class.getDeclaredMethod("restoreSuppressedPreProcessors"); + restoreMethod.setAccessible(true); + restoreMethod.invoke(sampler); + } +} diff --git a/t2/org/apache/jmeter/gui/tree/JMeterTreeListener.java b/t2/org/apache/jmeter/gui/tree/JMeterTreeListener.java deleted file mode 100644 index e2ccf37..0000000 --- a/t2/org/apache/jmeter/gui/tree/JMeterTreeListener.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * 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.gui.tree; - -import java.awt.Rectangle; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; - -import javax.swing.JPopupMenu; -import javax.swing.JTree; -import javax.swing.SwingUtilities; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.TreePath; - -import org.apache.jmeter.gui.GuiPackage; -import org.apache.jmeter.gui.MainFrame; -import org.apache.jmeter.gui.action.ActionNames; -import org.apache.jmeter.gui.action.ActionRouter; -import org.apache.jmeter.gui.action.KeyStrokes; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class JMeterTreeListener implements TreeSelectionListener, MouseListener, KeyListener { - private static final Logger log = LoggerFactory.getLogger(JMeterTreeListener.class); - - private TreePath currentPath; - - private ActionListener actionHandler; - - private JMeterTreeModel model; - - private JTree tree; - - /** - * Constructor for the JMeterTreeListener object. - * - * @param model - * The {@link JMeterTreeModel} for this listener - */ - public JMeterTreeListener(JMeterTreeModel model) { - this.model = model; - } - - /** - * Constructor for the {@link JMeterTreeListener} object - */ - public JMeterTreeListener() { - } - - /** - * Set the {@link JMeterTreeModel} for this listener - * @param m The {@link JMeterTreeModel} to be used - */ - public void setModel(JMeterTreeModel m) { - model = m; - } - - /** - * Sets the ActionHandler attribute of the JMeterTreeListener object. - * - * @param ah - * the new ActionHandler value - */ - public void setActionHandler(ActionListener ah) { - actionHandler = ah; - } - - /** - * Sets the JTree attribute of the JMeterTreeListener object. - * - * @param tree - * the new JTree value - */ - public void setJTree(JTree tree) { - this.tree = tree; - } - - /** - * Gets the JTree attribute of the JMeterTreeListener object. - * - * @return tree the current JTree value. - */ - public JTree getJTree() { - return tree; - } - - /** - * Gets the CurrentNode attribute of the JMeterTreeListener object. - * - * @return the CurrentNode value - */ - public JMeterTreeNode getCurrentNode() { - if (currentPath != null) { - if (currentPath.getLastPathComponent() != null) { - return (JMeterTreeNode) currentPath.getLastPathComponent(); - } - return (JMeterTreeNode) currentPath.getParentPath().getLastPathComponent(); - } - return (JMeterTreeNode) model.getRoot(); - } - - public JMeterTreeNode[] getSelectedNodes() { - TreePath[] paths = tree.getSelectionPaths(); - if (paths == null) { - return new JMeterTreeNode[] { getCurrentNode() }; - } - JMeterTreeNode[] nodes = new JMeterTreeNode[paths.length]; - for (int i = 0; i < paths.length; i++) { - nodes[i] = (JMeterTreeNode) paths[i].getLastPathComponent(); - } - - return nodes; - } - - public TreePath removedSelectedNode() { - currentPath = currentPath.getParentPath(); - return currentPath; - } - - @Override - public void valueChanged(TreeSelectionEvent e) { - log.debug("value changed, updating currentPath"); - currentPath = e.getNewLeadSelectionPath(); - // Call requestFocusInWindow to ensure current component loses focus and - // all values are correctly saved - // see https://bz.apache.org/bugzilla/show_bug.cgi?id=55103 - // see https://bz.apache.org/bugzilla/show_bug.cgi?id=55459 - tree.requestFocusInWindow(); - actionHandler.actionPerformed(new ActionEvent(this, 3333, ActionNames.EDIT)); // $NON-NLS-1$ - } - - @Override - public void mouseClicked(MouseEvent ev) { - } - - @Override - public void mouseReleased(MouseEvent e) { - } - - @Override - public void mouseEntered(MouseEvent e) { - } - - @Override - public void mousePressed(MouseEvent e) { - // Get the Main Frame. - MainFrame mainFrame = GuiPackage.getInstance().getMainFrame(); - // Close any Main Menu that is open - mainFrame.closeMenu(); - TreePath closestPath = tree.getClosestPathForLocation(e.getX(), e.getY()); - if (closestPath == null) { - log.debug("ClosestPathForLocation is not found for x={}, y={}", e.getX(), e.getY()); - return; - } - Rectangle bounds = tree.getPathBounds(closestPath); - if (bounds == null || bounds.y > e.getY() || e.getY() > bounds.y + bounds.height) { - log.debug("Mouse click was outside of node {}. bounds={}, event.x={}, event.y={}", - closestPath, bounds, e.getX(), e.getY()); - return; - } - currentPath = closestPath; - if (isRightClick(e)) { - if (tree.getSelectionCount() < 2) { - tree.setSelectionPath(currentPath); - } - log.debug("About to display pop-up"); - displayPopUp(e); - } - } - - @Override - public void mouseExited(MouseEvent ev) { - } - - @Override - public void keyPressed(KeyEvent e) { - String actionName = null; - - if (KeyStrokes.matches(e, KeyStrokes.COPY)) { - actionName = ActionNames.COPY; - } else if (KeyStrokes.matches(e, KeyStrokes.PASTE)) { - actionName = ActionNames.PASTE; - } else if (KeyStrokes.matches(e, KeyStrokes.CUT)) { - actionName = ActionNames.CUT; - } else if (KeyStrokes.matches(e, KeyStrokes.DUPLICATE)) { - actionName = ActionNames.DUPLICATE; - } else if (KeyStrokes.matches(e, KeyStrokes.ALT_UP_ARROW)) { - actionName = ActionNames.MOVE_UP; - } else if (KeyStrokes.matches(e, KeyStrokes.ALT_DOWN_ARROW)) { - actionName = ActionNames.MOVE_DOWN; - } else if (KeyStrokes.matches(e, KeyStrokes.ALT_LEFT_ARROW)) { - actionName = ActionNames.MOVE_LEFT; - } else if (KeyStrokes.matches(e, KeyStrokes.ALT_RIGHT_ARROW)) { - actionName = ActionNames.MOVE_RIGHT; - } else if (KeyStrokes.matches(e, KeyStrokes.SHIFT_LEFT_ARROW) - || KeyStrokes.matches(e, KeyStrokes.COLLAPSE_ALL_SUBTRACT)) { - actionName = ActionNames.COLLAPSE; - } else if (KeyStrokes.matches(e, KeyStrokes.SHIFT_RIGHT_ARROW) - || KeyStrokes.matches(e, KeyStrokes.EXPAND_ALL_SUBTRACT)) { - actionName = ActionNames.EXPAND; - } - - if (actionName != null) { - final ActionRouter actionRouter = ActionRouter.getInstance(); - actionRouter.doActionNow(new ActionEvent(e.getSource(), e.getID(), actionName)); - e.consume(); - } - } - - @Override - public void keyReleased(KeyEvent e) { - } - - @Override - public void keyTyped(KeyEvent e) { - } - - private boolean isRightClick(MouseEvent e) { - return e.isPopupTrigger() || - (InputEvent.BUTTON2_DOWN_MASK & e.getModifiersEx()) > 0 || - (InputEvent.BUTTON3_DOWN_MASK == e.getModifiersEx()); - } - - private void displayPopUp(MouseEvent e) { - JPopupMenu pop = getCurrentNode().createPopupMenu(); - // invokeLater ensures popup does not disappear when user right-clicks an inactive node - // In other words: right-click different nodes and verify if menu is shown every time. - // invokeLater seems to be required as long as tree.requestFocusInWindow(); is used - // in valueChanged - SwingUtilities.invokeLater(() -> GuiPackage.getInstance().displayPopUp(e, pop)); - } -}