You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by pm...@apache.org on 2017/12/18 12:51:09 UTC

svn commit: r1818562 - in /jmeter/trunk: src/components/org/apache/jmeter/extractor/BoundaryExtractor.java src/components/org/apache/jmeter/visualizers/RenderAsBoundaryExtractor.java test/src/org/apache/jmeter/extractor/BoundaryExtractorSpec.groovy

Author: pmouawad
Date: Mon Dec 18 12:51:09 2017
New Revision: 1818562

URL: http://svn.apache.org/viewvc?rev=1818562&view=rev
Log:
Bug 61852 - Add a Boundary Extractor Tester in View Results Tree
BoundaryExtractor refactor
Contributed by Graham Russell
This closes #355
Bugzilla Id: 61852

Added:
    jmeter/trunk/test/src/org/apache/jmeter/extractor/BoundaryExtractorSpec.groovy
Modified:
    jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java
    jmeter/trunk/src/components/org/apache/jmeter/visualizers/RenderAsBoundaryExtractor.java

Modified: jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java?rev=1818562&r1=1818561&r2=1818562&view=diff
==============================================================================
--- jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java (original)
+++ jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java Mon Dec 18 12:51:09 2017
@@ -22,6 +22,9 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -37,30 +40,23 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * 
+ * Extracts Strings from a text response between a start and end boundary.
  */
 public class BoundaryExtractor extends AbstractScopedTestElement implements PostProcessor, Serializable {
 
     private static final Logger log = LoggerFactory.getLogger(BoundaryExtractor.class);
 
-    private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 2L;
 
     private static final String REFNAME = "BoundaryExtractor.refname"; // $NON-NLS-1$
-
     private static final String MATCH_NUMBER = "BoundaryExtractor.match_number"; // $NON-NLS-1$
-
     private static final String L_BOUNDARY = "BoundaryExtractor.lboundary"; // $NON-NLS-1$
-
     private static final String R_BOUNDARY = "BoundaryExtractor.rboundary"; // $NON-NLS-1$
-
     private static final String DEFAULT_EMPTY_VALUE = "BoundaryExtractor.default_empty_value"; // $NON-NLS-1$
-
     private static final String DEFAULT = "BoundaryExtractor.default"; // $NON-NLS-1$
-
     private static final String REF_MATCH_NR = "_matchNr"; // $NON-NLS-1$
-    
     private static final char UNDERSCORE = '_';  // $NON-NLS-1$
-    
+
     // What to match against. N.B. do not change the string value or test plans will break!
     private static final String MATCH_AGAINST = "BoundaryExtractor.useHeaders"; // $NON-NLS-1$
     /*
@@ -71,21 +67,21 @@ public class BoundaryExtractor extends A
      *  These are passed to the setUseField() method
      *
      *  Do not change these values!
-    */
-    public static final String USE_HDRS = "true"; // $NON-NLS-1$
-    public static final String USE_REQUEST_HDRS = "request_headers"; // $NON-NLS-1$
-    public static final String USE_BODY = "false"; // $NON-NLS-1$
-    public static final String USE_BODY_UNESCAPED = "unescaped"; // $NON-NLS-1$
-    public static final String USE_BODY_AS_DOCUMENT = "as_document"; // $NON-NLS-1$
-    public static final String USE_URL = "URL"; // $NON-NLS-1$
-    public static final String USE_CODE = "code"; // $NON-NLS-1$
-    public static final String USE_MESSAGE = "message"; // $NON-NLS-1$
+     */
+    private static final String USE_HDRS = "true"; // $NON-NLS-1$
+    private static final String USE_REQUEST_HDRS = "request_headers"; // $NON-NLS-1$
+    private static final String USE_BODY = "false"; // $NON-NLS-1$
+    private static final String USE_BODY_UNESCAPED = "unescaped"; // $NON-NLS-1$
+    private static final String USE_BODY_AS_DOCUMENT = "as_document"; // $NON-NLS-1$
+    private static final String USE_URL = "URL"; // $NON-NLS-1$
+    private static final String USE_CODE = "code"; // $NON-NLS-1$
+    private static final String USE_MESSAGE = "message"; // $NON-NLS-1$
 
     /**
      * Parses the response data using Boundaries and saving the results
      * into variables for use later in the test.
      *
-     * @see org.apache.jmeter.processor.PostProcessor#process()
+     * @see PostProcessor#process()
      */
     @Override
     public void process() {
@@ -94,66 +90,108 @@ public class BoundaryExtractor extends A
         if (previousResult == null) {
             return;
         }
-        if(log.isDebugEnabled()) {
+        if (log.isDebugEnabled()) {
             log.debug("Boundary Extractor {}: processing result", getName());
         }
-        if(StringUtils.isAnyEmpty(getLeftBoundary(), getRightBoundary(), getRefName())) {
-            throw new IllegalArgumentException("One of the mandatory properties is missing in Boundary Extractor:"+
-                getName());
+        if (StringUtils.isAnyEmpty(getLeftBoundary(), getRightBoundary(), getRefName())) {
+            throw new IllegalArgumentException(
+                    "One of the mandatory properties is missing in Boundary Extractor:" + getName());
         }
-        // Fetch some variables
+
         JMeterVariables vars = context.getVariables();
-        
+
         String refName = getRefName();
-        int matchNumber = getMatchNumber();
         final String defaultValue = getDefaultValue();
-        
-        if (defaultValue.length() > 0  || isEmptyDefaultValue()){// Only replace default if it is provided or empty default value is explicitly requested
+
+        if (StringUtils.isNotBlank(defaultValue) || isEmptyDefaultValue()) {
             vars.put(refName, defaultValue);
         }
-        
-        try {            
-            List<String> matches = 
-                    extractMatchingStrings(vars, getLeftBoundary(), getRightBoundary(), matchNumber, previousResult);
-            int prevCount = 0;
-            String prevString = vars.get(refName + REF_MATCH_NR);
-            if (prevString != null) {
-                vars.remove(refName + REF_MATCH_NR);// ensure old value is not left defined
-                try {
-                    prevCount = Integer.parseInt(prevString);
-                } catch (NumberFormatException nfe) {
-                    if (log.isWarnEnabled()) {
-                        log.warn("{}: Could not parse number: '{}'.", getName(), prevString);
-                    }
-                }
-            }
-            int matchCount=0;// Number of refName_n variable sets to keep
-            String match;
-            if (matchNumber >= 0) {// Original match behaviour
-                match = getCorrectMatch(matches, matchNumber);
-                if (match != null) {
-                    vars.put(refName, match);
-                } 
-            } else // < 0 means we save all the matches
-            {
-                matchCount = matches.size();
-                vars.put(refName + REF_MATCH_NR, Integer.toString(matchCount));// Save the count
-                for (int i = 1; i <= matchCount; i++) {
-                    match = getCorrectMatch(matches, i);
-                    if (match != null) {
-                        final String refNameN = new StringBuilder(refName).append(UNDERSCORE).append(i).toString();
-                        vars.put(refNameN, match);
-                    }
-                }
+
+        int matchNumber = getMatchNumber();
+        int prevCount = 0;
+        int matchCount = 0;
+        try {
+            prevCount = removePrevCount(vars, refName);
+            List<String> matches = extractMatches(previousResult, vars, matchNumber);
+            matchCount = saveMatches(vars, refName, matchNumber, matchCount, matches);
+        } catch (RuntimeException e) {
+            if (log.isWarnEnabled()) {
+                log.warn("{}: Error while generating result. {}", getName(), e.toString()); // NOSONAR We don't want to be too verbose
             }
+        } finally {
             // Remove any left-over variables
             for (int i = matchCount + 1; i <= prevCount; i++) {
-                final String refNameN = new StringBuilder(refName).append(UNDERSCORE).append(i).toString();
-                vars.remove(refNameN);
+                vars.remove(refName + UNDERSCORE + i);
             }
-        } catch (RuntimeException e) {
-            if (log.isWarnEnabled()) {
-                log.warn("{}: Error while generating result. {}", getName(), e.toString()); // NOSONAR We don't want to be too verbose
+        }
+    }
+
+    private int removePrevCount(JMeterVariables vars, String refName) {
+        int prevCount = 0;
+        String prevString = vars.get(refName + REF_MATCH_NR);
+        if (prevString != null) {
+            // ensure old value is not left defined
+            vars.remove(refName + REF_MATCH_NR);
+            try {
+                prevCount = Integer.parseInt(prevString);
+            } catch (NumberFormatException nfe) {
+                if (log.isWarnEnabled()) {
+                    log.warn("{}: Could not parse number: '{}'.", getName(), prevString);
+                }
+            }
+        }
+        return prevCount;
+    }
+
+    private List<String> extractMatches(SampleResult previousResult, JMeterVariables vars, int matchNumber) {
+        if (isScopeVariable()) {
+            String inputString = vars.get(getVariableName());
+            if (inputString == null && log.isWarnEnabled()) {
+                log.warn("No variable '{}' found to process by Boundary Extractor '{}', skipping processing",
+                        getVariableName(), getName());
+            }
+            return extract(getLeftBoundary(), getRightBoundary(), matchNumber, inputString);
+        } else {
+            Stream<String> inputs = getSampleList(previousResult).stream().map(this::getInputString);
+            return extract(getLeftBoundary(), getRightBoundary(), matchNumber, inputs);
+        }
+    }
+
+    private int saveMatches(JMeterVariables vars, String refName, int matchNumber, int matchCount, List<String> matches) {
+        if (matchNumber == 0) {
+            saveRandomMatch(vars, refName, matches);
+        } else if (matchNumber > 0) {
+            saveOneMatch(vars, refName, matches);
+        } else {
+            matchCount = matches.size();
+            saveAllMatches(vars, refName, matches);
+        }
+        return matchCount;
+    }
+
+    private void saveRandomMatch(JMeterVariables vars, String refName, List<String> matches) {
+        String match = matches.get(JMeterUtils.getRandomInt(matches.size()));
+        if (match != null) {
+            vars.put(refName, match);
+        }
+    }
+
+    private void saveOneMatch(JMeterVariables vars, String refName, List<String> matches) {
+        if (matches.size() == 1) { // if not then invalid matchNum was likely supplied
+            String match = matches.get(0);
+            if (match != null) {
+                vars.put(refName, match);
+            }
+        }
+    }
+
+    private void saveAllMatches(JMeterVariables vars, String refName, List<String> matches) {
+        vars.put(refName + REF_MATCH_NR, Integer.toString(matches.size()));
+        for (int i = 0; i < matches.size(); i++) {
+            String match = matches.get(i);
+            if (match != null) {
+                int varNum = i + 1;
+                vars.put(refName + UNDERSCORE + varNum, match);
             }
         }
     }
@@ -168,117 +206,65 @@ public class BoundaryExtractor extends A
                 : useBodyAsDocument() ? Document.getTextFromDocument(result.getResponseData())
                 : result.getResponseDataAsString() // Bug 36898
                 ;
-       log.debug("Input = '{}'", inputString);
-       return inputString;
-    }
-    /**
-     * Grab the appropriate result from the list.
-     *
-     * @param matches
-     *            list of matches
-     * @param entry
-     *            the entry number in the list
-     * @return MatchResult
-     */
-    private String getCorrectMatch(List<String> matches, int entry) {
-        int matchSize = matches.size();
-
-        if (matchSize <= 0 || entry > matchSize){
-            return null;
-        }
-
-        if (entry == 0) // Random match
-        {
-            return matches.get(JMeterUtils.getRandomInt(matchSize));
-        }
-
-        return matches.get(entry - 1);
+        log.debug("Input = '{}'", inputString);
+        return inputString;
     }
 
-    private List<String> extractMatchingStrings(JMeterVariables vars,
-            String leftBoundary, String rightBoundary, int matchNumber,
-            SampleResult previousResult) {
-        int found = 0;
-        List<String> result = new ArrayList<>();
-        if (isScopeVariable()){
-            String inputString=vars.get(getVariableName());
-            if(!StringUtils.isEmpty(inputString)) {
-                extract(leftBoundary, rightBoundary, matchNumber, inputString, result, found);
-            } else {
-                if(inputString==null) {
-                    if (log.isWarnEnabled()) {
-                        log.warn("No variable '{}' found to process by Boundary Extractor '{}', skipping processing",
-                                getVariableName(), getName());
-                    }
-                }
-                return Collections.emptyList();
-            } 
-        } else {
-            List<SampleResult> sampleList = getSampleList(previousResult);
-            for (SampleResult sr : sampleList) {
-                String inputString = getInputString(sr);
-                found = extract(leftBoundary, rightBoundary, matchNumber, inputString, result, found);
-                if (matchNumber > 0 && found == matchNumber){// no need to process further
-                    break;
-                }
-            }
-        }
-        return result;
+    private List<String> extract(
+            String leftBoundary, String rightBoundary, int matchNumber, Stream<String> previousResults) {
+        boolean allItems = matchNumber <= 0;
+        return previousResults
+                .flatMap(input -> extractAll(leftBoundary, rightBoundary, input).stream())
+                .skip(allItems ? 0L : matchNumber - 1)
+                .limit(allItems ? Long.MAX_VALUE : 1L)
+                .collect(Collectors.toList());
     }
 
     /**
-     * Extracts text fragments, that are between the boundaries, into
-     * {@code result}.<br>
-     * The number of extracted fragments can be controlled by
-     * {@code matchNumber}
+     * Extracts text fragments, that are between the boundaries, into {@code result}.
+     * The number of extracted fragments can be controlled by {@code matchNumber}
      *
-     * @param leftBoundary
-     *            fragment representing the left boundary of the searched text
-     * @param rightBoundary
-     *            fragment representing the right boundary of the searched text
-     * @param matchNumber
-     *            if &lt; 0, all found matches will be added, else only those
-     *            matches, where {@code matchNumber} is less then {@code found}
-     *            plus the number of matches found in this round
-     * @param inputString
-     *            text in which to look for the fragments
-     * @param result
-     *            list where the found text fragments will be placed
-     * @param found
-     *            specifies how many fragments where found before calling us
-     * @return updated found (found plus number of fragements added to
-     *         {@code result})
+     * @param leftBoundary  fragment representing the left boundary of the searched text
+     * @param rightBoundary fragment representing the right boundary of the searched text
+     * @param matchNumber   if {@code <=0}, all found matches will be returned, else only
+     *                      up to {@code matchNumber} matches
+     * @param inputString   text in which to look for the fragments
+     * @return list where the found text fragments will be placed
      */
-    public int extract(String leftBoundary, String rightBoundary, int matchNumber, String inputString,
-            List<String> result, int found) {
-        int startIndex = -1;
-        int endIndex;
-        int newFound = found;
-        final int leftBoundarylength = leftBoundary.length();
+    private List<String> extract(String leftBoundary, String rightBoundary, int matchNumber, String inputString) {
+        if (StringUtils.isBlank(inputString)) {
+            return Collections.emptyList();
+        }
+        Objects.requireNonNull(leftBoundary);
+        Objects.requireNonNull(rightBoundary);
+
         List<String> matches = new ArrayList<>();
-        while(true) {
-            startIndex = inputString.indexOf(leftBoundary, startIndex+1);
-            if(startIndex >= 0) {
-                endIndex = inputString.indexOf(rightBoundary, startIndex+leftBoundarylength);
-                if(endIndex >= 0) {
-                    matches.add(inputString.substring(startIndex+leftBoundarylength, endIndex));
-                } else {
-                    break;
+        int leftBoundaryLen = leftBoundary.length();
+        boolean collectAll = matchNumber <= 0;
+        int found = 0;
+
+        for (int startIndex = 0;
+             (startIndex = inputString.indexOf(leftBoundary, startIndex)) != -1;
+             startIndex += leftBoundaryLen) {
+            int endIndex = inputString.indexOf(rightBoundary, startIndex + leftBoundaryLen);
+            if (endIndex >= 0) {
+                found++;
+                if (collectAll) {
+                    matches.add(inputString.substring(startIndex + leftBoundaryLen, endIndex));
+                } else if (found == matchNumber) {
+                    return Collections.singletonList(inputString.substring(startIndex + leftBoundaryLen, endIndex));
                 }
             } else {
                 break;
             }
         }
 
-        for (String element : matches) {
-            if (matchNumber <= 0 || newFound != matchNumber) {
-                result.add(element);
-                newFound++;
-            } else {
-                break;
-            }
-        }
-        return newFound;
+        return matches;
+    }
+
+    public List<String> extractAll(
+            String leftBoundary, String rightBoundary, String textToParse) {
+        return extract(leftBoundary, rightBoundary, -1, textToParse);
     }
 
     public void setRefName(String refName) {
@@ -310,19 +296,19 @@ public class BoundaryExtractor extends A
     public String getMatchNumberAsString() {
         return getPropertyAsString(MATCH_NUMBER);
     }
-    
+
     public void setLeftBoundary(String leftBoundary) {
         setProperty(L_BOUNDARY, leftBoundary);
     }
-    
+
     public String getLeftBoundary() {
         return getPropertyAsString(L_BOUNDARY);
     }
-    
+
     public void setRightBoundary(String rightBoundary) {
         setProperty(R_BOUNDARY, rightBoundary);
     }
-    
+
     public String getRightBoundary() {
         return getPropertyAsString(R_BOUNDARY);
     }
@@ -342,15 +328,16 @@ public class BoundaryExtractor extends A
     public void setDefaultEmptyValue(boolean defaultEmptyValue) {
         setProperty(DEFAULT_EMPTY_VALUE, defaultEmptyValue);
     }
-    
+
     /**
      * Get the default value for the variable if no matches are found
+     *
      * @return The default value for the variable
      */
     public String getDefaultValue() {
         return getPropertyAsString(DEFAULT);
     }
-    
+
     /**
      * @return boolean set value to "" if not found
      */
@@ -358,29 +345,27 @@ public class BoundaryExtractor extends A
         return getPropertyAsBoolean(DEFAULT_EMPTY_VALUE);
     }
 
-
     public boolean useHeaders() {
-        return USE_HDRS.equalsIgnoreCase( getPropertyAsString(MATCH_AGAINST));
+        return USE_HDRS.equalsIgnoreCase(getPropertyAsString(MATCH_AGAINST));
     }
 
     public boolean useRequestHeaders() {
         return USE_REQUEST_HDRS.equalsIgnoreCase(getPropertyAsString(MATCH_AGAINST));
     }
 
-    // Allow for property not yet being set (probably only applies to Test cases)
     public boolean useBody() {
         String prop = getPropertyAsString(MATCH_AGAINST);
-        return prop.length()==0 || USE_BODY.equalsIgnoreCase(prop);// $NON-NLS-1$
+        return prop.length() == 0 || USE_BODY.equalsIgnoreCase(prop);
     }
 
     public boolean useUnescapedBody() {
         String prop = getPropertyAsString(MATCH_AGAINST);
-        return USE_BODY_UNESCAPED.equalsIgnoreCase(prop);// $NON-NLS-1$
+        return USE_BODY_UNESCAPED.equalsIgnoreCase(prop);
     }
 
     public boolean useBodyAsDocument() {
         String prop = getPropertyAsString(MATCH_AGAINST);
-        return USE_BODY_AS_DOCUMENT.equalsIgnoreCase(prop);// $NON-NLS-1$
+        return USE_BODY_AS_DOCUMENT.equalsIgnoreCase(prop);
     }
 
     public boolean useUrl() {
@@ -399,6 +384,6 @@ public class BoundaryExtractor extends A
     }
 
     public void setUseField(String actionCommand) {
-        setProperty(MATCH_AGAINST,actionCommand);
+        setProperty(MATCH_AGAINST, actionCommand);
     }
 }

Modified: jmeter/trunk/src/components/org/apache/jmeter/visualizers/RenderAsBoundaryExtractor.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/components/org/apache/jmeter/visualizers/RenderAsBoundaryExtractor.java?rev=1818562&r1=1818561&r2=1818562&view=diff
==============================================================================
--- jmeter/trunk/src/components/org/apache/jmeter/visualizers/RenderAsBoundaryExtractor.java (original)
+++ jmeter/trunk/src/components/org/apache/jmeter/visualizers/RenderAsBoundaryExtractor.java Mon Dec 18 12:51:09 2017
@@ -23,7 +23,6 @@ import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.util.ArrayList;
 import java.util.List;
 
 import javax.swing.BoxLayout;
@@ -42,7 +41,6 @@ import org.apache.jmeter.util.JMeterUtil
 import org.apache.jorphan.gui.GuiUtils;
 import org.apache.jorphan.gui.JLabeledTextField;
 
-
 /**
  * Implement ResultsRender for Boundary Extractor tester
  */
@@ -95,17 +93,20 @@ public class RenderAsBoundaryExtractor i
     
     private String process(String textToParse) {
 
-        List<String> result = new ArrayList<>();
         BoundaryExtractor extractor = new BoundaryExtractor();
-        
-        final int nbFound = extractor.extract(boundaryExtractorFieldLeft.getText(),boundaryExtractorFieldRight.getText(), -1, textToParse, result, 0);
 
+        List<String> matches = extractor.extractAll(
+                boundaryExtractorFieldLeft.getText(),
+                boundaryExtractorFieldRight.getText(),
+                textToParse);
+
+        int nbFound = matches.size();
         // Construct a multi-line string with all matches
         StringBuilder sb = new StringBuilder();
         sb.append("Match count: ").append(nbFound).append("\n");
         for (int j = 0; j < nbFound; j++) {
-            String mr = result.get(j);
-            sb.append("Match[").append(j+1).append("]=").append(mr).append("\n");
+            String match = matches.get(j);
+            sb.append("Match[").append(j+1).append("]=").append(match).append("\n");
         }
         return sb.toString();
 

Added: jmeter/trunk/test/src/org/apache/jmeter/extractor/BoundaryExtractorSpec.groovy
URL: http://svn.apache.org/viewvc/jmeter/trunk/test/src/org/apache/jmeter/extractor/BoundaryExtractorSpec.groovy?rev=1818562&view=auto
==============================================================================
--- jmeter/trunk/test/src/org/apache/jmeter/extractor/BoundaryExtractorSpec.groovy (added)
+++ jmeter/trunk/test/src/org/apache/jmeter/extractor/BoundaryExtractorSpec.groovy Mon Dec 18 12:51:09 2017
@@ -0,0 +1,384 @@
+package org.apache.jmeter.extractor
+
+import org.apache.jmeter.samplers.SampleResult
+import org.apache.jmeter.threads.JMeterContext
+import org.apache.jmeter.threads.JMeterContextService
+import org.apache.jmeter.threads.JMeterVariables
+import spock.lang.Specification
+import spock.lang.Unroll
+
+import java.util.stream.Stream
+
+@Unroll
+class BoundaryExtractorSpec extends Specification {
+
+    static LEFT = "LB"
+    static RIGHT = "RB"
+    static DEFAULT_VAL = "defaultVal"
+    static VAR_NAME = "varName"
+
+    def sut = new BoundaryExtractor()
+
+    SampleResult prevResult
+    JMeterVariables vars
+    JMeterContext context
+
+    def setup() {
+        vars = new JMeterVariables()
+        context = JMeterContextService.getContext()
+        context.setVariables(vars)
+
+        sut.setThreadContext(context)
+        sut.setRefName(VAR_NAME)
+        sut.setLeftBoundary(LEFT)
+        sut.setRightBoundary(RIGHT)
+        sut.setDefaultValue(DEFAULT_VAL)
+        sut.setMatchNumber(1)
+
+        prevResult = new SampleResult()
+        prevResult.sampleStart()
+        prevResult.setResponseData(createInputString(1..2), null)
+        prevResult.sampleEnd()
+        context.setPreviousResult(prevResult)
+    }
+
+    def "Extract, where pattern exists, with matchNumber=#matchNumber from #occurances returns #expected"() {
+        given:
+            def input = createInputString(occurrences)
+        when:
+            def matches = sut.extract(LEFT, RIGHT, matchNumber, input)
+        then:
+            matches == expected
+        where:
+            occurrences | matchNumber || expected
+            1..1        | -1          || ['1']
+            1..1        | 0           || ['1']
+            1..1        | 1           || ['1']
+            1..1        | 2           || []
+            1..2        | -1          || ['1', '2']
+            1..2        | 1           || ['1']
+            1..2        | 2           || ['2']
+            1..3        | 3           || ['3']
+    }
+
+    def "Extract, where pattern does not exist, with matchNumber=#matchNumber returns an empty list"() {
+        expect:
+            sut.extract(LEFT, RIGHT, matchNumber, 'start end') == []
+        where:
+            matchNumber << [-1, 0, 1, 2, 100]
+    }
+
+    def "Extract, where pattern exists in the stream, with matchNumber=#matchNumber from #occurances returns #expected"() {
+        given:
+            def input = createInputString(occurrences)
+            Stream<String> stream = ([input, "", null] * 10).stream()
+        when:
+            def matches = sut.extract(LEFT, RIGHT, matchNumber, stream)
+        then:
+            matches == expected
+        where:
+            occurrences | matchNumber || expected
+            1..1        | -1          || ['1'] * 10
+            1..1        | 0           || ['1'] * 10
+            1..1        | 1           || ['1']
+            1..1        | 10          || ['1']
+            1..1        | 11          || []
+            1..2        | -1          || ['1', '2'] * 10
+            1..2        | 1           || ['1']
+            1..2        | 2           || ['2']
+            1..3        | 3           || ['3']
+    }
+
+    def "Extract, where pattern does not exist in the stream, with matchNumber=#matchNumber returns an empty list"() {
+        given:
+            Stream<String> stream = (['start end'] * 10).stream()
+        expect:
+            sut.extract(LEFT, RIGHT, matchNumber, stream) == []
+        where:
+            matchNumber << [-1, 0, 1, 2, 100]
+    }
+
+    def "IllegalArgumentException when one of left (#lb), right (#rb), name (#name) are null"() {
+        given:
+            sut.setLeftBoundary(lb)
+            sut.setRightBoundary(rb)
+            sut.setRefName(name)
+        when:
+            sut.process()
+        then:
+            thrown(IllegalArgumentException)
+        where:
+            lb   | rb   | name
+            null | "r"  | "name"
+            "l"  | null | "name"
+            "l"  | "r"  | null
+    }
+
+    def "matching only on left boundary returns default"() {
+        given:
+            sut.setRightBoundary("does-not-exist")
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == DEFAULT_VAL
+    }
+
+    def "matching only on right boundary returns default"() {
+        given:
+            sut.setLeftBoundary("does-not-exist")
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == DEFAULT_VAL
+    }
+
+    def "variables from a previous extraction are removed"() {
+        given:
+            sut.setMatchNumber(-1)
+            sut.process()
+            assert vars.get("${VAR_NAME}_1") == "1"
+            assert vars.get("${VAR_NAME}_matchNr") == "2"
+        when:
+            // Now rerun with match fail
+            sut.setMatchNumber(10)
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == DEFAULT_VAL
+            vars.get("${VAR_NAME}_1") == null
+            vars.get("${VAR_NAME}_matchNr") == null
+    }
+
+    def "with no sub-samples parent and all scope return data but children scope does not"() {
+        given:
+            sut.setScopeParent()
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == "1"
+
+        and:
+            sut.setScopeAll()
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == "1"
+
+        and:
+            sut.setScopeChildren()
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == DEFAULT_VAL
+    }
+
+    def "with sub-samples parent, all and children scope return expected item"() {
+        given:
+            prevResult.addSubResult(createSampleResult("${LEFT}sub1${RIGHT}"))
+            prevResult.addSubResult(createSampleResult("${LEFT}sub2${RIGHT}"))
+            prevResult.addSubResult(createSampleResult("${LEFT}sub3${RIGHT}"))
+            sut.setScopeParent()
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == "1"
+
+        and:
+            sut.setScopeAll()
+            sut.setMatchNumber(3) // skip 2 in parent sample
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == "sub1"
+
+        and:
+            sut.setScopeChildren()
+            sut.setMatchNumber(3)
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == "sub3"
+    }
+
+    def "with sub-samples parent, all and children scope return expected data"() {
+        given:
+            prevResult.addSubResult(createSampleResult("${LEFT}sub1${RIGHT}"))
+            prevResult.addSubResult(createSampleResult("${LEFT}sub2${RIGHT}"))
+            prevResult.addSubResult(createSampleResult("${LEFT}sub3${RIGHT}"))
+            sut.setMatchNumber(-1)
+
+            sut.setScopeParent()
+        when:
+            sut.process()
+        then:
+            vars.get("${VAR_NAME}_matchNr") == "2"
+            getAllVars() == ["1", "2"]
+
+        and:
+            sut.setScopeAll()
+        when:
+            sut.process()
+        then:
+            vars.get("${VAR_NAME}_matchNr") == "5"
+            getAllVars() == ["1", "2", "sub1", "sub2", "sub3"]
+
+        and:
+            sut.setScopeChildren()
+        when:
+            sut.process()
+        then:
+            vars.get("${VAR_NAME}_matchNr") == "3"
+            getAllVars() == ["sub1", "sub2", "sub3"]
+    }
+
+    def "when 'default empty value' is true the default value is allowed to be empty"() {
+        given:
+            sut.setMatchNumber(10) // no matches
+            sut.setDefaultValue("")
+            sut.setDefaultEmptyValue(true)
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == ""
+    }
+
+    def "when default value is empty but not allowed null is returned"() {
+        given:
+            sut.setMatchNumber(10) // no matches
+            sut.setDefaultValue("")
+            sut.setDefaultEmptyValue(false)
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == null
+    }
+
+    def "with no previous results result is null"() {
+        given:
+            context.setPreviousResult(null)
+            sut.setDefaultEmptyValue(true)
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == null
+    }
+
+    def "with non-existent variable result is null"() {
+        given:
+            sut.setDefaultValue(null)
+            sut.setScopeVariable("empty-var-name")
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == null
+    }
+
+    def "not allowing blank default value returns null upon no matches"() {
+        given:
+            sut.setMatchNumber(10) // no matches
+            sut.setDefaultValue("")
+            sut.setDefaultEmptyValue(false)
+        when:
+            sut.process()
+        then:
+            vars.get(VAR_NAME) == null
+    }
+
+    def "extract all matches from variable input"() {
+        given:
+            sut.setMatchNumber(-1)
+            sut.setScopeVariable("contentvar")
+            vars.put("contentvar", createInputString(1..2))
+        when:
+            sut.process()
+        then:
+            getAllVars() == ["1", "2"]
+            vars.get("${VAR_NAME}_matchNr") == "2"
+    }
+
+    def "extract random from variable returns one of the matches"() {
+        given:
+            sut.setMatchNumber(0)
+            sut.setScopeVariable("contentvar")
+            vars.put("contentvar", createInputString(1..42))
+        when:
+            sut.process()
+        then:
+            (1..42).collect({ it.toString() }).contains(vars.get(VAR_NAME))
+            vars.get("${VAR_NAME}_matchNr") == null
+    }
+
+    def "extract all from an empty variable returns no results"() {
+        given:
+            sut.setMatchNumber(-1)
+            sut.setScopeVariable("contentvar")
+            vars.put("contentvar", "")
+        when:
+            sut.process()
+        then:
+            vars.get("${VAR_NAME}_matchNr") == "0"
+            vars.get("${VAR_NAME}_1") == null
+    }
+
+    def "previous extractions are cleared"() {
+        given:
+            sut.setMatchNumber(-1)
+            sut.setScopeVariable("contentvar")
+            vars.put("contentvar", createInputString(1..10))
+            sut.process()
+            assert getAllVars() == (1..10).collect({ it.toString() })
+            assert vars.get("${VAR_NAME}_matchNr") == "10"
+            vars.put("contentvar", createInputString(11..15))
+            sut.setMatchNumber("-1")
+            def expectedMatches = (11..15).collect({ it.toString() })
+        when:
+            sut.process()
+        then:
+            getAllVars() == expectedMatches
+            vars.get("${VAR_NAME}_matchNr") == "5"
+            (6..10).collect { vars.get("${VAR_NAME}_${it}") } == [null] * 5
+
+        and:
+            sut.setMatchNumber(0)
+        when:
+            sut.process()
+        then:
+            expectedMatches.contains(vars.get(VAR_NAME))
+    }
+
+    /**
+     * Creates a string with a "match" for each number in the list.
+     *
+     * @param occurrences list of numbers to be the "body" of a match
+     * @return a string with a start, end and then a left boundary + number + right boundary
+     * e.g. "... LB1RB LB2RB ..."
+     */
+    static createInputString(List<Integer> occurrences) {
+        def middle = occurrences.collect({ LEFT + it + RIGHT }).join(" ")
+        return 'start \t\r\n' + middle + '\n\t end'
+    }
+
+    static createSampleResult(String responseData) {
+        SampleResult child = new SampleResult()
+        child.sampleStart()
+        child.setResponseData(responseData, "ISO-8859-1")
+        child.sampleEnd()
+        return child
+    }
+
+    /**
+     * @return a list of all the variables in the format ${VAR_NAME}_${i}
+     * starting at i = 1 until null is returned
+     */
+    def getAllVars() {
+        def allVars = []
+        def i = 1
+        def var = vars.get("${VAR_NAME}_${i}")
+        while (var != null) {
+            allVars.add(var)
+            i++
+            var = vars.get("${VAR_NAME}_${i}")
+        }
+        return allVars
+    }
+
+}