diff --git a/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java b/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java index 729c9d8ebd..1870c18a4b 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java @@ -158,6 +158,20 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl responseResult.setResponseTime(result.getTime()); responseResult.setResponseMessage(result.getResponseMessage()); + if (result.getVars() != null && !result.getVars().entrySet().isEmpty()) { + List vars = new LinkedList<>(); + result.getVars().entrySet().parallelStream().reduce(vars, (first, second) -> { + first.add(second.getKey() + ":" + second.getValue()); + return first; + }, (first, second) -> { + if (first == second) { + return first; + } + first.addAll(second); + return first; + }); + responseResult.setVars(StringUtils.join(vars, "\n")); + } for (AssertionResult assertionResult : result.getAssertionResults()) { ResponseAssertionResult responseAssertionResult = getResponseAssertionResult(assertionResult); if (responseAssertionResult.isPass()) { diff --git a/backend/src/main/java/io/metersphere/api/jmeter/ResponseResult.java b/backend/src/main/java/io/metersphere/api/jmeter/ResponseResult.java index f772278e17..01b37b741a 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/ResponseResult.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/ResponseResult.java @@ -23,6 +23,8 @@ public class ResponseResult { private String body; + private String vars; + private final List assertions = new ArrayList<>(); } diff --git a/backend/src/main/java/org/apache/jmeter/extractor/RegexExtractor.java b/backend/src/main/java/org/apache/jmeter/extractor/RegexExtractor.java new file mode 100644 index 0000000000..356b19ba1d --- /dev/null +++ b/backend/src/main/java/org/apache/jmeter/extractor/RegexExtractor.java @@ -0,0 +1,526 @@ + +/* + * 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.extractor; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.text.StringEscapeUtils; +import org.apache.jmeter.processor.PostProcessor; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.testelement.AbstractScopedTestElement; +import org.apache.jmeter.testelement.property.IntegerProperty; +import org.apache.jmeter.threads.JMeterContext; +import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jmeter.util.Document; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.oro.text.MalformedCachePatternException; +import org.apache.oro.text.regex.MatchResult; +import org.apache.oro.text.regex.Pattern; +import org.apache.oro.text.regex.PatternMatcher; +import org.apache.oro.text.regex.PatternMatcherInput; +import org.apache.oro.text.regex.Perl5Compiler; +import org.apache.oro.text.regex.Perl5Matcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RegexExtractor extends AbstractScopedTestElement implements PostProcessor, Serializable { + + private static final long serialVersionUID = 242L; + + private static final Logger log = LoggerFactory.getLogger(RegexExtractor.class); + + // What to match against. N.B. do not change the string value or test plans will break! + private static final String MATCH_AGAINST = "RegexExtractor.useHeaders"; // $NON-NLS-1$ + /* + * Permissible values: + * true - match against headers + * false or absent - match against body (this was the original default) + * URL - match against URL + * 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 REGEX_PROP = "RegexExtractor.regex"; // $NON-NLS-1$ + private static final String REFNAME_PROP = "RegexExtractor.refname"; // $NON-NLS-1$ + private static final String MATCH_NUMBER_PROP = "RegexExtractor.match_number"; // $NON-NLS-1$ + private static final String DEFAULT_PROP = "RegexExtractor.default"; // $NON-NLS-1$ + private static final String DEFAULT_EMPTY_VALUE_PROP = "RegexExtractor.default_empty_value"; // $NON-NLS-1$ + private static final String TEMPLATE_PROP = "RegexExtractor.template"; // $NON-NLS-1$ + + private static final String REF_MATCH_NR = "_matchNr"; // $NON-NLS-1$ + + private static final String UNDERSCORE = "_"; // $NON-NLS-1$ + + private static final boolean DEFAULT_VALUE_FOR_DEFAULT_EMPTY_VALUE = false; + + private transient List template; + + private JMeterVariables regexVars; + /** + * Parses the response data using regular expressions and saving the results + * into variables for use later in the test. + * + * @see org.apache.jmeter.processor.PostProcessor#process() + */ + @Override + public void process() { + initTemplate(); + regexVars = new JMeterVariables(); + JMeterContext context = getThreadContext(); + SampleResult previousResult = context.getPreviousResult(); + if (previousResult == null) { + return; + } + log.debug("RegexExtractor processing result"); + + // 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 + vars.put(refName, defaultValue); + regexVars.put(refName, defaultValue); + } + + Perl5Matcher matcher = JMeterUtils.getMatcher(); + String regex = getRegex(); + Pattern pattern = null; + try { + pattern = JMeterUtils.getPatternCache().getPattern(regex, Perl5Compiler.READ_ONLY_MASK); + List matches = processMatches(pattern, regex, previousResult, matchNumber, vars); + 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) { + log.warn("Could not parse number: '{}'", prevString); + } + } + int matchCount = 0;// Number of refName_n variable sets to keep + try { + MatchResult match; + if (matchNumber >= 0) {// Original match behaviour + match = getCorrectMatch(matches, matchNumber); + if (match != null) { + vars.put(refName, generateResult(match)); + regexVars.put(refName, generateResult(match)); + + saveGroups(vars, refName, match); + } else { + // refname has already been set to the default (if present) + removeGroups(vars, refName); + } + } else // < 0 means we save all the matches + { + removeGroups(vars, refName); // remove any single matches + matchCount = matches.size(); + vars.put(refName + REF_MATCH_NR, Integer.toString(matchCount));// Save the count + regexVars.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 refName_n = refName + UNDERSCORE + i; + vars.put(refName_n, generateResult(match)); + regexVars.put(refName_n, generateResult(match)); + + saveGroups(vars, refName_n, match); + } + } + } + // Remove any left-over variables + for (int i = matchCount + 1; i <= prevCount; i++) { + final String refName_n = refName + UNDERSCORE + i; + vars.remove(refName_n); + removeGroups(vars, refName_n); + } + previousResult.addVars(regexVars); + } catch (RuntimeException e) { + log.warn("Error while generating result"); + } + } catch (MalformedCachePatternException e) { + log.error("Error in pattern: '{}'", regex); + } finally { + JMeterUtils.clearMatcherMemory(matcher, pattern); + } + } + + private String getInputString(SampleResult result) { + String inputString = useUrl() ? result.getUrlAsString() // Bug 39707 + : useHeaders() ? result.getResponseHeaders() + : useRequestHeaders() ? result.getRequestHeaders() + : useCode() ? result.getResponseCode() // Bug 43451 + : useMessage() ? result.getResponseMessage() // Bug 43451 + : useUnescapedBody() ? StringEscapeUtils.unescapeHtml4(result.getResponseDataAsString()) + : useBodyAsDocument() ? Document.getTextFromDocument(result.getResponseData()) + : result.getResponseDataAsString() // Bug 36898 + ; + log.debug("Input = '{}'", inputString); + return inputString; + } + + private List processMatches(Pattern pattern, String regex, SampleResult result, int matchNumber, JMeterVariables vars) { + log.debug("Regex = '{}'", regex); + + Perl5Matcher matcher = JMeterUtils.getMatcher(); + List matches = new ArrayList<>(); + int found = 0; + + if (isScopeVariable()) { + String inputString = vars.get(getVariableName()); + if (inputString == null) { + if (log.isWarnEnabled()) { + log.warn("No variable '{}' found to process by RegexExtractor '{}', skipping processing", + getVariableName(), getName()); + } + return Collections.emptyList(); + } + matchStrings(matchNumber, matcher, pattern, matches, found, + inputString); + } else { + List sampleList = getSampleList(result); + for (SampleResult sr : sampleList) { + String inputString = getInputString(sr); + found = matchStrings(matchNumber, matcher, pattern, matches, found, + inputString); + if (matchNumber > 0 && found == matchNumber) {// no need to process further + break; + } + } + } + return matches; + } + + private int matchStrings(int matchNumber, Perl5Matcher matcher, + Pattern pattern, List matches, int found, + String inputString) { + PatternMatcherInput input = new PatternMatcherInput(inputString); + while (matchNumber <= 0 || found != matchNumber) { + if (matcher.contains(input, pattern)) { + log.debug("RegexExtractor: Match found!"); + matches.add(matcher.getMatch()); + found++; + } else { + break; + } + } + return found; + } + + /** + * Creates the variables:
+ * basename_gn, where n=0...# of groups
+ * basename_g = number of groups (apart from g0) + */ + private void saveGroups(JMeterVariables vars, String basename, MatchResult match) { + StringBuilder buf = new StringBuilder(); + buf.append(basename); + buf.append("_g"); // $NON-NLS-1$ + int pfxlen = buf.length(); + String prevString = vars.get(buf.toString()); + int previous = 0; + if (prevString != null) { + try { + previous = Integer.parseInt(prevString); + } catch (NumberFormatException nfe) { + log.warn("Could not parse number: '{}'.", prevString); + } + } + //Note: match.groups() includes group 0 + final int groups = match.groups(); + for (int x = 0; x < groups; x++) { + buf.append(x); + vars.put(buf.toString(), match.group(x)); + buf.setLength(pfxlen); + } + vars.put(buf.toString(), Integer.toString(groups - 1)); + for (int i = groups; i <= previous; i++) { + buf.append(i); + vars.remove(buf.toString());// remove the remaining _gn vars + buf.setLength(pfxlen); + } + } + + /** + * Removes the variables:
+ * basename_gn, where n=0...# of groups
+ * basename_g = number of groups (apart from g0) + */ + private void removeGroups(JMeterVariables vars, String basename) { + StringBuilder buf = new StringBuilder(); + buf.append(basename); + buf.append("_g"); // $NON-NLS-1$ + int pfxlen = buf.length(); + // How many groups are there? + int groups; + try { + groups = Integer.parseInt(vars.get(buf.toString())); + } catch (NumberFormatException e) { + groups = 0; + } + vars.remove(buf.toString());// Remove the group count + for (int i = 0; i <= groups; i++) { + buf.append(i); + vars.remove(buf.toString());// remove the g0,g1...gn vars + buf.setLength(pfxlen); + } + } + + private String generateResult(MatchResult match) { + StringBuilder result = new StringBuilder(); + for (Object obj : template) { + if (log.isDebugEnabled()) { + log.debug("RegexExtractor: Template piece {} ({})", obj, obj.getClass()); + } + if (obj instanceof Integer) { + result.append(match.group((Integer) obj)); + } else { + result.append(obj); + } + } + log.debug("Regex Extractor result = '{}'", result); + return result.toString(); + } + + private void initTemplate() { + if (template != null) { + return; + } + // Contains Strings and Integers + List combined = new ArrayList<>(); + String rawTemplate = getTemplate(); + PatternMatcher matcher = JMeterUtils.getMatcher(); + Pattern templatePattern = JMeterUtils.getPatternCache().getPattern("\\$(\\d+)\\$" // $NON-NLS-1$ + , Perl5Compiler.READ_ONLY_MASK + & Perl5Compiler.SINGLELINE_MASK); + if (log.isDebugEnabled()) { + log.debug("Pattern = '{}', template = '{}'", templatePattern.getPattern(), rawTemplate); + } + int beginOffset = 0; + MatchResult currentResult; + PatternMatcherInput pinput = new PatternMatcherInput(rawTemplate); + while (matcher.contains(pinput, templatePattern)) { + currentResult = matcher.getMatch(); + final int beginMatch = currentResult.beginOffset(0); + if (beginMatch > beginOffset) { // string is not empty + combined.add(rawTemplate.substring(beginOffset, beginMatch)); + } + combined.add(Integer.valueOf(currentResult.group(1)));// add match as Integer + beginOffset = currentResult.endOffset(0); + } + + if (beginOffset < rawTemplate.length()) { // trailing string is not empty + combined.add(rawTemplate.substring(beginOffset, rawTemplate.length())); + } + if (log.isDebugEnabled()) { + log.debug("Template item count: {}", combined.size()); + int i = 0; + for (Object o : combined) { + log.debug("Template item-{}: {} '{}'", i++, o.getClass(), o); + } + } + template = combined; + } + + /** + * Grab the appropriate result from the list. + * + * @param matches list of matches + * @param entry the entry number in the list + * @return MatchResult + */ + private MatchResult getCorrectMatch(List 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); + } + + /** + * Set the regex to be used + * + * @param regex The string representation of the regex + */ + public void setRegex(String regex) { + setProperty(REGEX_PROP, regex); + } + + /** + * Get the regex which is to be used + * + * @return string representing the regex + */ + public String getRegex() { + return getPropertyAsString(REGEX_PROP); + } + + /** + * Set the prefix name of the variable to be used to store the regex matches + * + * @param refName prefix of the variables to be used + */ + public void setRefName(String refName) { + setProperty(REFNAME_PROP, refName); + } + + /** + * Get the prefix name of the variable to be used to store the regex matches + * + * @return The prefix of the variables to be used + */ + public String getRefName() { + return getPropertyAsString(REFNAME_PROP); + } + + /** + * Set which Match to use. This can be any positive number, indicating the + * exact match to use, or 0, which is interpreted as meaning + * random. + * + * @param matchNumber The number of the match to be used, or 0 if a + * random match should be used. + */ + public void setMatchNumber(int matchNumber) { + setProperty(new IntegerProperty(MATCH_NUMBER_PROP, matchNumber)); + } + + public void setMatchNumber(String matchNumber) { + setProperty(MATCH_NUMBER_PROP, matchNumber); + } + + public int getMatchNumber() { + return getPropertyAsInt(MATCH_NUMBER_PROP); + } + + public String getMatchNumberAsString() { + return getPropertyAsString(MATCH_NUMBER_PROP); + } + + /** + * Sets the value of the variable if no matches are found + * + * @param defaultValue The default value for the variable + */ + public void setDefaultValue(String defaultValue) { + setProperty(DEFAULT_PROP, defaultValue); + } + + /** + * Set default value to "" value when if it's empty + * + * @param defaultEmptyValue The default value for the variable + */ + public void setDefaultEmptyValue(boolean defaultEmptyValue) { + setProperty(DEFAULT_EMPTY_VALUE_PROP, defaultEmptyValue, DEFAULT_VALUE_FOR_DEFAULT_EMPTY_VALUE); + } + + /** + * Get the default value for the variable, which should be used, if no + * matches are found + * + * @return The default value for the variable + */ + public String getDefaultValue() { + return getPropertyAsString(DEFAULT_PROP); + } + + /** + * Do we set default value to "" value when if it's empty + * + * @return true if we should set default value to "" if variable cannot be extracted + */ + public boolean isEmptyDefaultValue() { + return getPropertyAsBoolean(DEFAULT_EMPTY_VALUE_PROP, DEFAULT_VALUE_FOR_DEFAULT_EMPTY_VALUE); + } + + public void setTemplate(String template) { + setProperty(TEMPLATE_PROP, template); + } + + public String getTemplate() { + return getPropertyAsString(TEMPLATE_PROP); + } + + public boolean useHeaders() { + 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$ + } + + public boolean useUnescapedBody() { + String prop = getPropertyAsString(MATCH_AGAINST); + return USE_BODY_UNESCAPED.equalsIgnoreCase(prop);// $NON-NLS-1$ + } + + public boolean useBodyAsDocument() { + String prop = getPropertyAsString(MATCH_AGAINST); + return USE_BODY_AS_DOCUMENT.equalsIgnoreCase(prop);// $NON-NLS-1$ + } + + public boolean useUrl() { + String prop = getPropertyAsString(MATCH_AGAINST); + return USE_URL.equalsIgnoreCase(prop); + } + + public boolean useCode() { + String prop = getPropertyAsString(MATCH_AGAINST); + return USE_CODE.equalsIgnoreCase(prop); + } + + public boolean useMessage() { + String prop = getPropertyAsString(MATCH_AGAINST); + return USE_MESSAGE.equalsIgnoreCase(prop); + } + + public void setUseField(String actionCommand) { + setProperty(MATCH_AGAINST, actionCommand); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/apache/jmeter/extractor/XPath2Extractor.java b/backend/src/main/java/org/apache/jmeter/extractor/XPath2Extractor.java new file mode 100644 index 0000000000..4946ecafe2 --- /dev/null +++ b/backend/src/main/java/org/apache/jmeter/extractor/XPath2Extractor.java @@ -0,0 +1,283 @@ +/* + * 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.extractor; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.stream.FactoryConfigurationError; + +import org.apache.jmeter.assertions.AssertionResult; +import org.apache.jmeter.processor.PostProcessor; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.testelement.AbstractScopedTestElement; +import org.apache.jmeter.testelement.property.IntegerProperty; +import org.apache.jmeter.threads.JMeterContext; +import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jmeter.util.XPathUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sf.saxon.s9api.SaxonApiException; + +/** + * Extracts text from (X)HTML response using XPath query language + * Example XPath queries: + *
+ *
/html/head/title
+ *
extracts Title from HTML response
+ *
//form[@name='countryForm']//select[@name='country']/option[text()='Czech Republic'])/@value + *
extracts value attribute of option element that match text 'Czech Republic' + * inside of select element with name attribute 'country' inside of + * form with name attribute 'countryForm'
+ *
//head
+ *
extracts the XML fragment for head node.
+ *
//head/text()
+ *
extracts the text content for head node.
+ *
+ * see org.apache.jmeter.extractor.TestXPathExtractor for unit tests + */ +public class XPath2Extractor + extends AbstractScopedTestElement + implements PostProcessor, Serializable { + + private static final Logger log = LoggerFactory.getLogger(XPath2Extractor.class); + + private static final long serialVersionUID = 242L; + + private static final int DEFAULT_VALUE = 0; + public static final String DEFAULT_VALUE_AS_STRING = Integer.toString(DEFAULT_VALUE); + + private static final String REF_MATCH_NR = "matchNr"; // $NON-NLS-1$ + + //+ JMX file attributes + private static final String XPATH_QUERY = "XPathExtractor2.xpathQuery"; // $NON-NLS-1$ + private static final String REFNAME = "XPathExtractor2.refname"; // $NON-NLS-1$ + private static final String DEFAULT = "XPathExtractor2.default"; // $NON-NLS-1$ + private static final String FRAGMENT = "XPathExtractor2.fragment"; // $NON-NLS-1$ + private static final String NAMESPACES = "XPathExtractor2.namespaces"; // $NON-NLS-1$ + private static final String MATCH_NUMBER = "XPathExtractor2.matchNumber"; // $NON-NLS-1$ + private JMeterVariables regexVars; + + //- JMX file attributes + + private String concat(String s1, String s2) { + return s1 + "_" + s2; // $NON-NLS-1$ + } + + private String concat(String s1, int i) { + return s1 + "_" + i; // $NON-NLS-1$ + } + + /** + * Do the job - extract value from (X)HTML response using XPath Query. + * Return value as variable defined by REFNAME. Returns DEFAULT value + * if not found. + */ + @Override + public void process() { + JMeterContext context = getThreadContext(); + final SampleResult previousResult = context.getPreviousResult(); + if (previousResult == null) { + return; + } + JMeterVariables vars = context.getVariables(); + String refName = getRefName(); + vars.put(refName, getDefaultValue()); + final String matchNR = concat(refName, REF_MATCH_NR); + int prevCount = 0; // number of previous matches + try { + prevCount = Integer.parseInt(vars.get(matchNR)); + } catch (NumberFormatException e) { + // ignored + } + + vars.put(matchNR, "0"); // In case parse fails // $NON-NLS-1$ + vars.remove(concat(refName, "1")); // In case parse fails // $NON-NLS-1$ + + int matchNumber = getMatchNumber(); + List matches = new ArrayList<>(); + try { + if (isScopeVariable()) { + String inputString = vars.get(getVariableName()); + if (inputString != null) { + if (inputString.length() > 0) { + getValuesForXPath(getXPathQuery(), matches, matchNumber, inputString); + } + } else { + if (log.isWarnEnabled()) { + log.warn("No variable '{}' found to process by XPathExtractor '{}', skipping processing", + getVariableName(), getName()); + } + } + } else { + List samples = getSampleList(previousResult); + int size = samples.size(); + for (int i = 0; i < size; i++) { + getValuesForXPath(getXPathQuery(), matches, matchNumber, previousResult.getResponseDataAsString()); + } + } + final int matchCount = matches.size(); + vars.put(matchNR, String.valueOf(matchCount)); + if (matchCount > 0) { + String value = matches.get(0); + if (value != null) { + vars.put(refName, value); + } + for (int i = 0; i < matchCount; i++) { + value = matches.get(i); + if (value != null) { + vars.put(concat(refName, i + 1), matches.get(i)); + } + } + } + vars.remove(concat(refName, matchCount + 1)); // Just in case + // Clear any other remaining variables + for (int i = matchCount + 2; i <= prevCount; i++) { + vars.remove(concat(refName, i)); + } + previousResult.addVars(vars); + } catch (Exception e) {// Saxon exception + if (log.isWarnEnabled()) { + log.warn("Exception while processing '{}', message:{}", getXPathQuery(), e.getMessage()); + } + addAssertionFailure(previousResult, e, false); + } + } + + private void addAssertionFailure(final SampleResult previousResult, + final Throwable thrown, final boolean setFailed) { + AssertionResult ass = new AssertionResult(getName()); // $NON-NLS-1$ + ass.setFailure(true); + ass.setFailureMessage(thrown.getLocalizedMessage() + "\nSee log file for further details."); + previousResult.addAssertionResult(ass); + if (setFailed) { + previousResult.setSuccessful(false); + } + } + + /*============= object properties ================*/ + public void setXPathQuery(String val) { + setProperty(XPATH_QUERY, val); + } + + public String getXPathQuery() { + return getPropertyAsString(XPATH_QUERY); + } + + public void setRefName(String refName) { + setProperty(REFNAME, refName); + } + + public String getRefName() { + return getPropertyAsString(REFNAME); + } + + + public void setDefaultValue(String val) { + setProperty(DEFAULT, val); + } + + public String getDefaultValue() { + return getPropertyAsString(DEFAULT); + } + + /** + * Should we return fragment as text, rather than text of fragment? + * + * @return true if we should return fragment rather than text + */ + public boolean getFragment() { + return getPropertyAsBoolean(FRAGMENT, false); + } + + /** + * Should we return fragment as text, rather than text of fragment? + * + * @param selected true to return fragment. + */ + public void setFragment(boolean selected) { + setProperty(FRAGMENT, selected, false); + } + + /*================= internal business =================*/ + + /** + * Extract value from String responseData by XPath query. + * + * @param query the query to execute + * @param matchStrings list of matched strings (may include nulls) + * @param matchNumber int Match Number + * @param responseData String that contains the entire Document + * @throws SaxonApiException + * @throws FactoryConfigurationError + */ + private void getValuesForXPath(String query, List matchStrings, int matchNumber, String responseData) + throws SaxonApiException, FactoryConfigurationError { + XPathUtil.putValuesForXPathInListUsingSaxon(responseData, query, matchStrings, getFragment(), matchNumber, getNamespaces()); + } + + /** + * Set which Match to use. This can be any positive number, indicating the + * exact match to use, or 0, which is interpreted as meaning random. + * + * @param matchNumber The number of the match to be used + */ + public void setMatchNumber(int matchNumber) { + setProperty(new IntegerProperty(MATCH_NUMBER, matchNumber)); + } + + /** + * Set which Match to use. This can be any positive number, indicating the + * exact match to use, or 0, which is interpreted as meaning random. + * + * @param matchNumber The number of the match to be used + */ + public void setMatchNumber(String matchNumber) { + setProperty(MATCH_NUMBER, matchNumber); + } + + /** + * Return which Match to use. This can be any positive number, indicating the + * exact match to use, or 0, which is interpreted as meaning random. + * + * @return matchNumber The number of the match to be used + */ + public int getMatchNumber() { + return getPropertyAsInt(MATCH_NUMBER, DEFAULT_VALUE); + } + + /** + * Return which Match to use. This can be any positive number, indicating the + * exact match to use, or 0, which is interpreted as meaning random. + * + * @return matchNumber The number of the match to be used + */ + public String getMatchNumberAsString() { + return getPropertyAsString(MATCH_NUMBER, DEFAULT_VALUE_AS_STRING); + } + + public void setNamespaces(String namespaces) { + setProperty(NAMESPACES, namespaces); + } + + public String getNamespaces() { + return getPropertyAsString(NAMESPACES); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/apache/jmeter/extractor/json/jsonpath/JSONPostProcessor.java b/backend/src/main/java/org/apache/jmeter/extractor/json/jsonpath/JSONPostProcessor.java new file mode 100644 index 0000000000..f879b2f5fb --- /dev/null +++ b/backend/src/main/java/org/apache/jmeter/extractor/json/jsonpath/JSONPostProcessor.java @@ -0,0 +1,304 @@ +/* + * 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.extractor.json.jsonpath; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.processor.PostProcessor; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.testelement.AbstractScopedTestElement; +import org.apache.jmeter.testelement.ThreadListener; +import org.apache.jmeter.threads.JMeterContext; +import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jorphan.util.JOrphanUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JSON-PATH based extractor + * @since 3.0 + */ +public class JSONPostProcessor + extends AbstractScopedTestElement + implements Serializable, PostProcessor, ThreadListener { + + private static final long serialVersionUID = 1L; + private static final Logger log = LoggerFactory.getLogger(JSONPostProcessor.class); + + private static final String JSON_PATH_EXPRESSIONS = "JSONPostProcessor.jsonPathExprs"; // $NON-NLS-1$ + private static final String REFERENCE_NAMES = "JSONPostProcessor.referenceNames"; // $NON-NLS-1$ + private static final String DEFAULT_VALUES = "JSONPostProcessor.defaultValues"; // $NON-NLS-1$ + private static final String MATCH_NUMBERS = "JSONPostProcessor.match_numbers"; // $NON-NLS-1$ + private static final String COMPUTE_CONCATENATION = "JSONPostProcessor.compute_concat"; // $NON-NLS-1$ + private static final String REF_MATCH_NR = "_matchNr"; // $NON-NLS-1$ + private static final String ALL_SUFFIX = "_ALL"; // $NON-NLS-1$ + + private static final String JSON_CONCATENATION_SEPARATOR = ","; //$NON-NLS-1$ + private static final String SEPARATOR = ";"; // $NON-NLS-1$ + public static final boolean COMPUTE_CONCATENATION_DEFAULT_VALUE = false; + + private static final ThreadLocal localMatcher = ThreadLocal.withInitial(JSONManager::new); + + @Override + public void process() { + JMeterContext context = getThreadContext(); + JMeterVariables vars = context.getVariables(); + String jsonResponse = extractJsonResponse(context, vars); + String[] refNames = getRefNames().split(SEPARATOR); + String[] jsonPathExpressions = getJsonPathExpressions().split(SEPARATOR); + String[] defaultValues = getDefaultValues().split(SEPARATOR); + int[] matchNumbers = getMatchNumbersAsInt(defaultValues.length); + + validateSameLengthOfArguments(refNames, jsonPathExpressions, defaultValues); + + for (int i = 0; i < jsonPathExpressions.length; i++) { + int matchNumber = matchNumbers[i]; + String currentRefName = refNames[i].trim(); + String currentJsonPath = jsonPathExpressions[i].trim(); + clearOldRefVars(vars, currentRefName); + try { + if (StringUtils.isEmpty(jsonResponse)) { + handleEmptyResponse(vars, defaultValues, i, currentRefName); + } else { + List extractedValues = localMatcher.get() + .extractWithJsonPath(jsonResponse, currentJsonPath); + // if no values extracted, default value added + if (extractedValues.isEmpty()) { + handleEmptyResult(vars, defaultValues, i, matchNumber, currentRefName); + } else { + handleNonEmptyResult(vars, defaultValues, i, matchNumber, currentRefName, extractedValues); + } + } + SampleResult previousResult = context.getPreviousResult(); + previousResult.addVars(vars); + } catch (Exception e) { + // if something wrong, default value added + if (log.isDebugEnabled()) { + log.error("Error processing JSON content in {}, message: {}", getName(), e.getLocalizedMessage(), e); + } else { + log.error("Error processing JSON content in {}, message: {}", getName(), e.getLocalizedMessage()); + } + vars.put(currentRefName, defaultValues[i]); + } + } + } + + private void handleNonEmptyResult(JMeterVariables vars, String[] defaultValues, int i, int matchNumber, + String currentRefName, List extractedValues) { + // if more than one value extracted, suffix with "_index" + if (extractedValues.size() > 1) { + handleListResult(vars, defaultValues, i, matchNumber, currentRefName, extractedValues); + } else { + // else just one value extracted + handleSingleResult(vars, matchNumber, currentRefName, extractedValues); + } + if (matchNumber != 0) { + vars.put(currentRefName + REF_MATCH_NR, Integer.toString(extractedValues.size())); + } + } + + private void validateSameLengthOfArguments(String[] refNames, String[] jsonPathExpressions, + String[] defaultValues) { + if (refNames.length != jsonPathExpressions.length || + refNames.length != defaultValues.length) { + log.error( + "Number of JSON Path variables must match number of default values and json-path expressions," + + " check you use separator ';' if you have many values"); // $NON-NLS-1$ + throw new IllegalArgumentException(JMeterUtils + .getResString("jsonpp_error_number_arguments_mismatch_error")); // $NON-NLS-1$ + } + } + + private void handleSingleResult(JMeterVariables vars, final int matchNumber, String currentRefName, + List extractedValues) { + String suffix = (matchNumber < 0) ? "_1" : ""; + placeObjectIntoVars(vars, currentRefName + suffix, extractedValues, 0); + if (matchNumber < 0 && getComputeConcatenation()) { + vars.put(currentRefName + ALL_SUFFIX, vars.get(currentRefName + suffix)); + } + } + + private void handleListResult(JMeterVariables vars, String[] defaultValues, final int i, final int matchNumber, + String currentRefName, List extractedValues) { + if (matchNumber < 0) { + // Extract all + int index = 1; + StringBuilder concat = + new StringBuilder(getComputeConcatenation() + ? extractedValues.size() * 20 + : 1); + for (Object extractedObject : extractedValues) { + String extractedString = stringify(extractedObject); + vars.put(currentRefName + "_" + index, + extractedString); //$NON-NLS-1$ + if (getComputeConcatenation()) { + concat.append(extractedString) + .append(JSONPostProcessor.JSON_CONCATENATION_SEPARATOR); + } + index++; + } + if (getComputeConcatenation()) { + concat.setLength(concat.length() - 1); + vars.put(currentRefName + ALL_SUFFIX, concat.toString()); + } + return; + } + if (matchNumber == 0) { + // Random extraction + int matchSize = extractedValues.size(); + int matchNr = JMeterUtils.getRandomInt(matchSize); + placeObjectIntoVars(vars, currentRefName, + extractedValues, matchNr); + return; + } + // extract at position + if (matchNumber > extractedValues.size()) { + if(log.isDebugEnabled()) { + log.debug( + "matchNumber({}) exceeds number of items found({}), default value will be used", + matchNumber, extractedValues.size()); + } + vars.put(currentRefName, defaultValues[i]); + } else { + placeObjectIntoVars(vars, currentRefName, extractedValues, matchNumber - 1); + } + } + + private void handleEmptyResult(JMeterVariables vars, String[] defaultValues, int i, int matchNumber, + String currentRefName) { + vars.put(currentRefName, defaultValues[i]); + vars.put(currentRefName + REF_MATCH_NR, "0"); //$NON-NLS-1$ + if (matchNumber < 0 && getComputeConcatenation()) { + log.debug("No value extracted, storing empty in: {}{}", currentRefName, ALL_SUFFIX); + vars.put(currentRefName + ALL_SUFFIX, ""); + } + } + + private void handleEmptyResponse(JMeterVariables vars, String[] defaultValues, int i, String currentRefName) { + if(log.isDebugEnabled()) { + log.debug("Response or source variable is null or empty for {}", getName()); + } + vars.put(currentRefName, defaultValues[i]); + } + + private String extractJsonResponse(JMeterContext context, JMeterVariables vars) { + String jsonResponse = ""; + if (isScopeVariable()) { + jsonResponse = vars.get(getVariableName()); + if (log.isDebugEnabled()) { + log.debug("JSON Extractor is using variable: {}, which content is: {}", getVariableName(), jsonResponse); + } + } else { + SampleResult previousResult = context.getPreviousResult(); + if (previousResult != null) { + jsonResponse = previousResult.getResponseDataAsString(); + if (log.isDebugEnabled()) { + log.debug("JSON Extractor {} working on Response: {}", getName(), jsonResponse); + } + } + } + return jsonResponse; + } + + private void clearOldRefVars(JMeterVariables vars, String refName) { + vars.remove(refName + REF_MATCH_NR); + for (int i=1; vars.get(refName + "_" + i) != null; i++) { + vars.remove(refName + "_" + i); + } + } + + private void placeObjectIntoVars(JMeterVariables vars, String currentRefName, + List extractedValues, int matchNr) { + vars.put(currentRefName, + stringify(extractedValues.get(matchNr))); + } + + private String stringify(Object obj) { + return obj == null ? "" : obj.toString(); //$NON-NLS-1$ + } + + public String getJsonPathExpressions() { + return getPropertyAsString(JSON_PATH_EXPRESSIONS); + } + + public void setJsonPathExpressions(String jsonPath) { + setProperty(JSON_PATH_EXPRESSIONS, jsonPath); + } + + public String getRefNames() { + return getPropertyAsString(REFERENCE_NAMES); + } + + public void setRefNames(String refName) { + setProperty(REFERENCE_NAMES, refName); + } + + public String getDefaultValues() { + return getPropertyAsString(DEFAULT_VALUES); + } + + public void setDefaultValues(String defaultValue) { + setProperty(DEFAULT_VALUES, defaultValue, ""); // $NON-NLS-1$ + } + + public boolean getComputeConcatenation() { + return getPropertyAsBoolean(COMPUTE_CONCATENATION, COMPUTE_CONCATENATION_DEFAULT_VALUE); + } + + public void setComputeConcatenation(boolean computeConcatenation) { + setProperty(COMPUTE_CONCATENATION, computeConcatenation, COMPUTE_CONCATENATION_DEFAULT_VALUE); + } + + @Override + public void threadStarted() { + // NOOP + } + + @Override + public void threadFinished() { + localMatcher.remove(); + } + + public void setMatchNumbers(String matchNumber) { + setProperty(MATCH_NUMBERS, matchNumber); + } + + public String getMatchNumbers() { + return getPropertyAsString(MATCH_NUMBERS); + } + + public int[] getMatchNumbersAsInt(int arraySize) { + + String matchNumbersAsString = getMatchNumbers(); + int[] result = new int[arraySize]; + if (JOrphanUtils.isBlank(matchNumbersAsString)) { + Arrays.fill(result, 0); + } else { + String[] matchNumbersAsStringArray = + matchNumbersAsString.split(SEPARATOR); + for (int i = 0; i < matchNumbersAsStringArray.length; i++) { + result[i] = Integer.parseInt(matchNumbersAsStringArray[i].trim()); + } + } + return result; + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/apache/jmeter/samplers/SampleResult.java b/backend/src/main/java/org/apache/jmeter/samplers/SampleResult.java new file mode 100644 index 0000000000..4d1c3af09e --- /dev/null +++ b/backend/src/main/java/org/apache/jmeter/samplers/SampleResult.java @@ -0,0 +1,1622 @@ +/* + * 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.samplers; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.apache.jmeter.assertions.AssertionResult; +import org.apache.jmeter.gui.Searchable; +import org.apache.jmeter.testelement.TestPlan; +import org.apache.jmeter.threads.JMeterContext.TestLogicalAction; +import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jorphan.util.JOrphanUtils; +import org.aspectj.weaver.ast.Var; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// For unit tests, @see TestSampleResult + +/** + * This is a nice packaging for the various information returned from taking a + * sample of an entry. + */ +public class SampleResult implements Serializable, Cloneable, Searchable { + + private static final long serialVersionUID = 241L; + + // Needs to be accessible from Test code + static Logger log = LoggerFactory.getLogger(SampleResult.class); + + /** + * The default encoding to be used if not overridden. + * The value is ISO-8859-1. + */ + public static final String DEFAULT_HTTP_ENCODING = StandardCharsets.ISO_8859_1.name(); + + private static final String OK_CODE = Integer.toString(HttpURLConnection.HTTP_OK); + private static final String OK_MSG = "OK"; // $NON-NLS-1$ + private static final String INVALID_CALL_SEQUENCE_MSG = "Invalid call sequence"; // $NON-NLS-1$ + + + // Bug 33196 - encoding ISO-8859-1 is only suitable for Western countries + // However the suggested System.getProperty("file.encoding") is Cp1252 on + // Windows + // So use a new property with the original value as default + // needs to be accessible from test code + /** + * The default encoding to be used to decode the responseData byte array. + * The value is defined by the property "sampleresult.default.encoding" + * with a default of DEFAULT_HTTP_ENCODING if that is not defined. + */ + protected static final String DEFAULT_ENCODING + = JMeterUtils.getPropDefault("sampleresult.default.encoding", // $NON-NLS-1$ + DEFAULT_HTTP_ENCODING); + + /** + * The default used by {@link #setResponseData(String, String)} + */ + private static final String DEFAULT_CHARSET = Charset.defaultCharset().name(); + + /** + * Data type value ({@value}) indicating that the response data is text. + * + * @see #getDataType + * @see #setDataType(java.lang.String) + */ + public static final String TEXT = "text"; // $NON-NLS-1$ + + /** + * Data type value ({@value}) indicating that the response data is binary. + * + * @see #getDataType + * @see #setDataType(java.lang.String) + */ + public static final String BINARY = "bin"; // $NON-NLS-1$ + + private static final boolean DISABLE_SUBRESULTS_RENAMING = JMeterUtils.getPropDefault("subresults.disable_renaming", false); + + // List of types that are known to be binary + private static final String[] BINARY_TYPES = { + "image/", //$NON-NLS-1$ + "audio/", //$NON-NLS-1$ + "video/", //$NON-NLS-1$ + }; + + // List of types that are known to be ascii, although they may appear to be binary + private static final String[] NON_BINARY_TYPES = { + "audio/x-mpegurl", //$NON-NLS-1$ (HLS Media Manifest) + "audio/mpegurl", //$NON-NLS-1$ (HLS Media Manifest) + "video/f4m", //$NON-NLS-1$ (Flash Media Manifest) + "image/svg+xml" //$NON-NLS-1$ (SVG is xml) + }; + + + /** + * empty array which can be returned instead of null + */ + private static final byte[] EMPTY_BA = new byte[0]; + + private static final SampleResult[] EMPTY_SR = new SampleResult[0]; + + private static final AssertionResult[] EMPTY_AR = new AssertionResult[0]; + + private static final boolean START_TIMESTAMP = + JMeterUtils.getPropDefault("sampleresult.timestamp.start", false); // $NON-NLS-1$ + + /** + * Allow read-only access from test code + */ + private static final boolean USE_NANO_TIME = + JMeterUtils.getPropDefault("sampleresult.useNanoTime", true); // $NON-NLS-1$ + + /** + * How long between checks of nanotime; default 5000ms; set to <=0 to disable the thread + */ + private static final long NANOTHREAD_SLEEP = + JMeterUtils.getPropDefault("sampleresult.nanoThreadSleep", 5000); // $NON-NLS-1$ + + private static final String NULL_FILENAME = "NULL"; + + static { + if (START_TIMESTAMP) { + log.info("Note: Sample TimeStamps are START times"); + } else { + log.info("Note: Sample TimeStamps are END times"); + } + log.info("sampleresult.default.encoding is set to {}", DEFAULT_ENCODING); + log.info("sampleresult.useNanoTime={}", USE_NANO_TIME); + log.info("sampleresult.nanoThreadSleep={}", NANOTHREAD_SLEEP); + + if (USE_NANO_TIME && NANOTHREAD_SLEEP > 0) { + // Make sure we start with a reasonable value + NanoOffset.nanoOffset = System.currentTimeMillis() - SampleResult.sampleNsClockInMs(); + NanoOffset nanoOffset = new NanoOffset(); + nanoOffset.setDaemon(true); + nanoOffset.setName("NanoOffset"); + nanoOffset.start(); + } + } + + private SampleSaveConfiguration saveConfig; + + private SampleResult parent; + + private byte[] responseData = EMPTY_BA; + + private String responseCode = "";// Never return null + + private String label = "";// Never return null + + /** + * Filename used by ResultSaver + */ + private String resultFileName = ""; + + /** + * The data used by the sampler + */ + private String samplerData; + + private String threadName = ""; // Never return null + + private String responseMessage = ""; + + private String responseHeaders = ""; // Never return null + + private String requestHeaders = ""; + + /** + * timeStamp == 0 means either not yet initialised or no stamp available (e.g. when loading a results file) + * the time stamp - can be start or end + */ + private long timeStamp = 0; + + private long startTime = 0; + + private long endTime = 0; + + private long idleTime = 0;// Allow for non-sample time + + /** + * Start of pause (if any) + */ + private long pauseTime = 0; + + private List assertionResults; + + private List subResults; + + private JMeterVariables vars; + + /** + * The data type of the sample + * + * @see #getDataType() + * @see #setDataType(String) + * @see #TEXT + * @see #BINARY + */ + private String dataType = ""; // Don't return null if not set + + private boolean success; + + /** + * Files that this sample has been saved in. + * In Non GUI mode and when best config is used, size never exceeds 1, + * but as a compromise set it to 2 + */ + private final Set files = ConcurrentHashMap.newKeySet(2); + + // TODO do contentType and/or dataEncoding belong in HTTPSampleResult instead? + private String dataEncoding;// (is this really the character set?) e.g. + // ISO-8895-1, UTF-8 + + private String contentType = ""; // e.g. text/html; charset=utf-8 + + /** + * elapsed time + */ + private long elapsedTime = 0; + + /** + * time to first response + */ + private long latency = 0; + + /** + * time to end connecting + */ + private long connectTime = 0; + + /** + * Way to signal what to do on Test + */ + private TestLogicalAction testLogicalAction = TestLogicalAction.CONTINUE; + + /** + * Should thread terminate? + */ + private boolean stopThread = false; + + /** + * Should test terminate? + */ + private boolean stopTest = false; + + /** + * Should test terminate abruptly? + */ + private boolean stopTestNow = false; + + private int sampleCount = 1; + + private long bytes = 0; // Allows override of sample size in case sampler does not want to store all the data + + private int headersSize = 0; + + private long bodySize = 0; + + /** + * Currently active threads in this thread group + */ + private volatile int groupThreads = 0; + + /** + * Currently active threads in all thread groups + */ + private volatile int allThreads = 0; + + private final long nanoTimeOffset; + + // Allow testcode access to the settings + final boolean useNanoTime; + + final long nanoThreadSleep; + + private long sentBytes; + + private URL location; + + private transient boolean ignore; + + private transient int subResultIndex; + + /** + * Cache for responseData as string to avoid multiple computations + */ + private transient volatile String responseDataAsString; + + public SampleResult() { + this(USE_NANO_TIME, NANOTHREAD_SLEEP); + } + + // Allow test code to change the default useNanoTime setting + SampleResult(boolean nanoTime) { + this(nanoTime, NANOTHREAD_SLEEP); + } + + // Allow test code to change the default useNanoTime and nanoThreadSleep settings + SampleResult(boolean nanoTime, long nanoThreadSleep) { + this.elapsedTime = 0; + this.useNanoTime = nanoTime; + this.nanoThreadSleep = nanoThreadSleep; + this.nanoTimeOffset = initOffset(); + } + + /** + * Copy constructor. + * + * @param res existing sample result + */ + public SampleResult(SampleResult res) { + this(); + allThreads = res.allThreads;//OK + assertionResults = res.assertionResults; + bytes = res.bytes; + headersSize = res.headersSize; + bodySize = res.bodySize; + contentType = res.contentType;//OK + dataEncoding = res.dataEncoding;//OK + dataType = res.dataType;//OK + endTime = res.endTime;//OK + // files is created automatically, and applies per instance + groupThreads = res.groupThreads;//OK + idleTime = res.idleTime; + label = res.label;//OK + latency = res.latency; + connectTime = res.connectTime; + location = res.location;//OK + parent = res.parent; + pauseTime = res.pauseTime; + requestHeaders = res.requestHeaders;//OK + responseCode = res.responseCode;//OK + responseData = res.responseData;//OK + responseDataAsString = null; + responseHeaders = res.responseHeaders;//OK + responseMessage = res.responseMessage;//OK + + // Don't copy this; it is per instance resultFileName = res.resultFileName; + + sampleCount = res.sampleCount; + samplerData = res.samplerData; + saveConfig = res.saveConfig; + sentBytes = res.sentBytes; + startTime = res.startTime;//OK + stopTest = res.stopTest; + stopTestNow = res.stopTestNow; + stopThread = res.stopThread; + testLogicalAction = res.testLogicalAction; + subResults = res.subResults; + success = res.success;//OK + threadName = res.threadName;//OK + elapsedTime = res.elapsedTime; + timeStamp = res.timeStamp; + } + + /** + * Create a sample with a specific elapsed time but don't allow the times to + * be changed later + *

+ * (only used by HTTPSampleResult) + * + * @param elapsed time + * @param atend create the sample finishing now, else starting now + */ + protected SampleResult(long elapsed, boolean atend) { + this(); + long now = currentTimeInMillis(); + if (atend) { + setTimes(now - elapsed, now); + } else { + setTimes(now, now + elapsed); + } + } + + /** + * Allow users to create a sample with specific timestamp and elapsed times + * for cloning purposes, but don't allow the times to be changed later + *

+ * Currently used by CSVSaveService and + * StatisticalSampleResult + * + * @param stamp this may be a start time or an end time (both in + * milliseconds) + * @param elapsed time in milliseconds + */ + public SampleResult(long stamp, long elapsed) { + this(); + stampAndTime(stamp, elapsed); + } + + private long initOffset() { + if (useNanoTime) { + return nanoThreadSleep > 0 ? NanoOffset.getNanoOffset() : System.currentTimeMillis() - sampleNsClockInMs(); + } else { + return Long.MIN_VALUE; + } + } + + /** + * @param propertiesToSave The propertiesToSave to set. + */ + public void setSaveConfig(SampleSaveConfiguration propertiesToSave) { + this.saveConfig = propertiesToSave; + } + + public SampleSaveConfiguration getSaveConfig() { + return saveConfig; + } + + public boolean isStampedAtStart() { + return START_TIMESTAMP; + } + + /** + * Create a sample with specific start and end times for test purposes, but + * don't allow the times to be changed later + *

+ * (used by StatVisualizerModel.Test) + * + * @param start start time in milliseconds since unix epoch + * @param end end time in milliseconds since unix epoch + * @return sample with given start and end time + */ + public static SampleResult createTestSample(long start, long end) { + SampleResult res = new SampleResult(); + res.setStartTime(start); + res.setEndTime(end); + return res; + } + + /** + * Create a sample with a specific elapsed time for test purposes, but don't + * allow the times to be changed later + * + * @param elapsed - desired elapsed time in milliseconds + * @return sample that starts 'now' and ends elapsed milliseconds later + */ + public static SampleResult createTestSample(long elapsed) { + long now = System.currentTimeMillis(); + return createTestSample(now, now + elapsed); + } + + private static long sampleNsClockInMs() { + return System.nanoTime() / 1000000; + } + + /** + * Helper method to get 1 ms resolution timing. + * + * @return the current time in milliseconds + * @throws RuntimeException when useNanoTime is true but + * nanoTimeOffset is not set + */ + public long currentTimeInMillis() { + if (useNanoTime) { + if (nanoTimeOffset == Long.MIN_VALUE) { + throw new IllegalStateException("Invalid call; nanoTimeOffset has not been set"); + } + return sampleNsClockInMs() + nanoTimeOffset; + } + return System.currentTimeMillis(); + } + + // Helper method to maintain timestamp relationships + private void stampAndTime(long stamp, long elapsed) { + if (START_TIMESTAMP) { + startTime = stamp; + endTime = stamp + elapsed; + } else { + startTime = stamp - elapsed; + endTime = stamp; + } + timeStamp = stamp; + elapsedTime = elapsed; + } + + /** + * For use by SaveService only. + * + * @param stamp this may be a start time or an end time (both in milliseconds) + * @param elapsed time in milliseconds + * @throws RuntimeException when startTime or endTime has been + * set already + */ + public void setStampAndTime(long stamp, long elapsed) { + if (startTime != 0 || endTime != 0) { + throw new IllegalStateException("Calling setStampAndTime() after start/end times have been set"); + } + stampAndTime(stamp, elapsed); + } + + /** + * Set the "marked" flag to show that the result has been written to the file. + * + * @param filename the name of the file + * @return true if the result was previously marked + */ + public boolean markFile(String filename) { + return !files.add(filename != null ? filename : NULL_FILENAME); + } + + public String getResponseCode() { + return responseCode; + } + + /** + * Set response code to OK, i.e. "200" + */ + public void setResponseCodeOK() { + responseCode = OK_CODE; + } + + public void setResponseCode(String code) { + responseCode = code; + } + + public boolean isResponseCodeOK() { + return responseCode.equals(OK_CODE); + } + + public String getResponseMessage() { + return responseMessage; + } + + public void setResponseMessage(String msg) { + responseMessage = msg; + } + + public void setResponseMessageOK() { + responseMessage = OK_MSG; + } + + /** + * Set result statuses OK - shorthand method to set: + *

    + *
  • ResponseCode
  • + *
  • ResponseMessage
  • + *
  • Successful status
  • + *
+ */ + public void setResponseOK() { + setResponseCodeOK(); + setResponseMessageOK(); + setSuccessful(true); + } + + public String getThreadName() { + return threadName; + } + + public void setThreadName(String threadName) { + this.threadName = threadName; + } + + /** + * Get the sample timestamp, which may be either the start time or the end time. + * + * @return timeStamp in milliseconds + * @see #getStartTime() + * @see #getEndTime() + */ + public long getTimeStamp() { + return timeStamp; + } + + public String getSampleLabel() { + return label; + } + + /** + * Get the sample label for use in summary reports etc. + * + * @param includeGroup whether to include the thread group name + * @return the label + */ + public String getSampleLabel(boolean includeGroup) { + if (includeGroup) { + return threadName.substring(0, threadName.lastIndexOf(' ')) + ":" + label; + } + return label; + } + + public void setSampleLabel(String label) { + this.label = label; + } + + public void addAssertionResult(AssertionResult assertResult) { + if (assertionResults == null) { + assertionResults = new ArrayList<>(); + } + assertionResults.add(assertResult); + } + + /** + * Gets the assertion results associated with this sample. + * + * @return an array containing the assertion results for this sample. + * Returns empty array if there are no assertion results. + */ + public AssertionResult[] getAssertionResults() { + if (assertionResults == null) { + return EMPTY_AR; + } + return assertionResults.toArray(new AssertionResult[assertionResults.size()]); + } + + /** + * Add a subresult and adjust the parent byte count and end-time. + * + * @param subResult the {@link SampleResult} to be added + */ + public void addSubResult(SampleResult subResult) { + addSubResult(subResult, isRenameSampleLabel()); + } + + /** + * see https://bz.apache.org/bugzilla/show_bug.cgi?id=63055 + * + * @return true if TestPlan is in functional mode or property subresults.disable_renaming is true + */ + public static boolean isRenameSampleLabel() { + return !(TestPlan.getFunctionalMode() || DISABLE_SUBRESULTS_RENAMING); + } + + /** + * Add a subresult and adjust the parent byte count and end-time. + * + * @param subResult the {@link SampleResult} to be added + * @param renameSubResults boolean do we rename subResults based on position + */ + public void addSubResult(SampleResult subResult, boolean renameSubResults) { + if (subResult == null) { + // see https://bz.apache.org/bugzilla/show_bug.cgi?id=54778 + return; + } + String tn = getThreadName(); + if (tn.length() == 0) { + tn = Thread.currentThread().getName(); + this.setThreadName(tn); + } + subResult.setThreadName(tn); + + // Extend the time to the end of the added sample + setEndTime(Math.max(getEndTime(), subResult.getEndTime() + nanoTimeOffset - subResult.nanoTimeOffset)); // Bug 51855 + // Include the byte count for the added sample + setBytes(getBytesAsLong() + subResult.getBytesAsLong()); + setSentBytes(getSentBytes() + subResult.getSentBytes()); + setHeadersSize(getHeadersSize() + subResult.getHeadersSize()); + setBodySize(getBodySizeAsLong() + subResult.getBodySizeAsLong()); + addRawSubResult(subResult, renameSubResults); + } + + /** + * Add a subresult to the collection without updating any parent fields. + * + * @param subResult the {@link SampleResult} to be added + */ + public void addRawSubResult(SampleResult subResult) { + storeSubResult(subResult, isRenameSampleLabel()); + } + + /** + * Add a subresult to the collection without updating any parent fields. + * + * @param subResult the {@link SampleResult} to be added + */ + private void addRawSubResult(SampleResult subResult, boolean renameSubResults) { + storeSubResult(subResult, renameSubResults); + } + + /** + * Add a subresult read from a results file. + *

+ * As for {@link SampleResult#addSubResult(SampleResult) + * addSubResult(SampleResult)}, except that the fields don't need to be + * accumulated + * + * @param subResult the {@link SampleResult} to be added + */ + public void storeSubResult(SampleResult subResult) { + storeSubResult(subResult, isRenameSampleLabel()); + } + + /** + * Add a subresult read from a results file. + *

+ * As for {@link SampleResult#addSubResult(SampleResult) + * addSubResult(SampleResult)}, except that the fields don't need to be + * accumulated + * + * @param subResult the {@link SampleResult} to be added + * @param renameSubResults boolean do we rename subResults based on position + */ + private void storeSubResult(SampleResult subResult, boolean renameSubResults) { + if (subResults == null) { + subResults = new ArrayList<>(); + } + if (renameSubResults) { + subResult.setSampleLabel(getSampleLabel() + "-" + subResultIndex++); + } + subResults.add(subResult); + subResult.setParent(this); + } + + /** + * Gets the subresults associated with this sample. + * + * @return an array containing the subresults for this sample. Returns an + * empty array if there are no subresults. + */ + public SampleResult[] getSubResults() { + if (subResults == null) { + return EMPTY_SR; + } + return subResults.toArray(new SampleResult[subResults.size()]); + } + + /** + * Sets the responseData attribute of the SampleResult object. + *

+ * If the parameter is null, then the responseData is set to an empty byte array. + * This ensures that getResponseData() can never be null. + * + * @param response the new responseData value + */ + public void setResponseData(byte[] response) { + responseDataAsString = null; + responseData = response == null ? EMPTY_BA : response; + } + + /** + * Sets the responseData attribute of the SampleResult object. + * Should only be called after setting the dataEncoding (if necessary) + * + * @param response the new responseData value (String) + * @deprecated - only intended for use from BeanShell code + */ + @Deprecated + public void setResponseData(String response) { + responseDataAsString = null; + try { + responseData = response.getBytes(getDataEncodingWithDefault()); + } catch (UnsupportedEncodingException e) { + log.warn("Could not convert string, using default encoding. " + e.getLocalizedMessage()); + responseData = response.getBytes(Charset.defaultCharset()); // N.B. default charset is used deliberately here + } + } + + /** + * Sets the encoding and responseData attributes of the SampleResult object. + * + * @param response the new responseData value (String) + * @param encoding the encoding to set and then use (if null, use platform default) + */ + public void setResponseData(final String response, final String encoding) { + responseDataAsString = null; + String encodeUsing = encoding != null ? encoding : DEFAULT_CHARSET; + try { + responseData = response.getBytes(encodeUsing); + setDataEncoding(encodeUsing); + } catch (UnsupportedEncodingException e) { + log.warn("Could not convert string using '" + encodeUsing + + "', using default encoding: " + DEFAULT_CHARSET, e); + responseData = response.getBytes(Charset.defaultCharset()); // N.B. default charset is used deliberately here + setDataEncoding(DEFAULT_CHARSET); + } + } + + /** + * Gets the responseData attribute of the SampleResult object. + *

+ * Note that some samplers may not store all the data, in which case + * getResponseData().length will be incorrect. + *

+ * Instead, always use {@link #getBytes()} to obtain the sample result byte count. + *

+ * + * @return the responseData value (cannot be null) + */ + public byte[] getResponseData() { + return responseData; + } + + /** + * Gets the responseData of the SampleResult object as a String + * + * @return the responseData value as a String, converted according to the encoding + */ + public String getResponseDataAsString() { + try { + if (responseDataAsString == null) { + responseDataAsString = new String(responseData, getDataEncodingWithDefault()); + } + return responseDataAsString; + } catch (UnsupportedEncodingException e) { + log.warn("Using platform default as " + getDataEncodingWithDefault() + " caused " + e); + return new String(responseData, Charset.defaultCharset()); // N.B. default charset is used deliberately here + } + } + + public void setSamplerData(String s) { + samplerData = s; + } + + public String getSamplerData() { + return samplerData; + } + + /** + * Get the time it took this sample to occur. + * + * @return elapsed time in milliseconds + */ + public long getTime() { + return elapsedTime; + } + + public boolean isSuccessful() { + return success; + } + + /** + * Sets the data type of the sample. + * + * @param dataType String containing {@link #BINARY} or {@link #TEXT} + * @see #BINARY + * @see #TEXT + */ + public void setDataType(String dataType) { + this.dataType = dataType; + } + + /** + * Returns the data type of the sample. + * + * @return String containing {@link #BINARY} or {@link #TEXT} or the empty string + * @see #BINARY + * @see #TEXT + */ + public String getDataType() { + return dataType; + } + + /** + * Extract and save the DataEncoding and DataType from the parameter provided. + * Does not save the full content Type. + * + * @param ct - content type (may be null) + * @see #setContentType(String) which should be used to save the full content-type string + */ + public void setEncodingAndType(String ct) { + if (ct != null) { + // Extract charset and store as DataEncoding + // N.B. The meta tag: + // + // is now processed by HTTPSampleResult#getDataEncodingWithDefault + final String charsetPrefix = "charset="; // $NON-NLS-1$ + int cset = ct.toLowerCase(java.util.Locale.ENGLISH).indexOf(charsetPrefix); + if (cset >= 0) { + String charSet = ct.substring(cset + charsetPrefix.length()); + // handle: ContentType: text/plain; charset=ISO-8859-1; format=flowed + int semiColon = charSet.indexOf(';'); + if (semiColon >= 0) { + charSet = charSet.substring(0, semiColon); + } + // Check for quoted string + if (charSet.startsWith("\"") || charSet.startsWith("\'")) { // $NON-NLS-1$ + setDataEncoding(charSet.substring(1, charSet.length() - 1)); // remove quotes + } else { + setDataEncoding(charSet); + } + } + if (isBinaryType(ct)) { + setDataType(BINARY); + } else { + setDataType(TEXT); + } + } + } + + /* + * Determine if content-type is known to be binary, i.e. not displayable as text. + * + * @param ct content type + * @return true if content-type is of type binary. + */ + public static boolean isBinaryType(String ct) { + for (String entry : NON_BINARY_TYPES) { + if (ct.startsWith(entry)) { + return false; + } + } + for (String binaryType : BINARY_TYPES) { + if (ct.startsWith(binaryType)) { + return true; + } + } + return false; + } + + /** + * Sets the successful attribute of the SampleResult object. + * + * @param success the new successful value + */ + public void setSuccessful(boolean success) { + this.success = success; + } + + /** + * Returns the display name. + * + * @return display name of this sample result + */ + @Override + public String toString() { + return getSampleLabel(); + } + + /** + * Returns the dataEncoding or the default if no dataEncoding was provided. + * + * @return the value of the dataEncoding or DEFAULT_ENCODING + */ + public String getDataEncodingWithDefault() { + return getDataEncodingWithDefault(DEFAULT_ENCODING); + } + + /** + * Returns the dataEncoding or the default if no dataEncoding was provided. + * + * @param defaultEncoding the default to be applied + * @return the value of the dataEncoding or the provided default + */ + protected String getDataEncodingWithDefault(String defaultEncoding) { + if (dataEncoding != null && dataEncoding.length() > 0) { + return dataEncoding; + } + return defaultEncoding; + } + + /** + * Returns the dataEncoding. May be null or the empty String. + * + * @return the value of the dataEncoding + */ + public String getDataEncodingNoDefault() { + return dataEncoding; + } + + /** + * Sets the dataEncoding. + * + * @param dataEncoding the dataEncoding to set, e.g. ISO-8895-1, UTF-8 + */ + public void setDataEncoding(String dataEncoding) { + this.dataEncoding = dataEncoding; + } + + /** + * @return whether to stop the test waiting for current running Sampler to end + */ + public boolean isStopTest() { + return stopTest; + } + + /** + * @return whether to stop the test now interrupting current running samplers + */ + public boolean isStopTestNow() { + return stopTestNow; + } + + /** + * @return whether to stop this thread + */ + public boolean isStopThread() { + return stopThread; + } + + public void setStopTest(boolean b) { + stopTest = b; + } + + public void setStopTestNow(boolean b) { + stopTestNow = b; + } + + public void setStopThread(boolean b) { + stopThread = b; + } + + /** + * @return the request headers + */ + public String getRequestHeaders() { + return requestHeaders; + } + + /** + * @return the response headers + */ + public String getResponseHeaders() { + return responseHeaders; + } + + /** + * @param string - + * request headers + */ + public void setRequestHeaders(String string) { + requestHeaders = string; + } + + /** + * @param string - + * response headers + */ + public void setResponseHeaders(String string) { + responseHeaders = string; + } + + /** + * @return the full content type - e.g. text/html [;charset=utf-8 ] + */ + public String getContentType() { + return contentType; + } + + /** + * Get the media type from the Content Type + * + * @return the media type - e.g. text/html (without charset, if any) + */ + public String getMediaType() { + return JOrphanUtils.trim(contentType, " ;").toLowerCase(java.util.Locale.ENGLISH); + } + + /** + * Stores the content-type string, e.g. text/xml; charset=utf-8 + * + * @param string the content-type to be set + * @see #setEncodingAndType(String) which can be used to extract the charset. + */ + public void setContentType(String string) { + contentType = string; + } + + /** + * @return idleTime + */ + public long getIdleTime() { + return idleTime; + } + + /** + * @return the end time + */ + public long getEndTime() { + return endTime; + } + + /** + * @return the start time + */ + public long getStartTime() { + return startTime; + } + + /* + * Helper methods N.B. setStartTime must be called before setEndTime + * + * setStartTime is used by HTTPSampleResult to clone the parent sampler and + * allow the original start time to be kept + */ + protected final void setStartTime(long start) { + startTime = start; + if (START_TIMESTAMP) { + timeStamp = startTime; + } + } + + public void setEndTime(long end) { + endTime = end; + if (!START_TIMESTAMP) { + timeStamp = endTime; + } + if (startTime == 0) { + log.error("setEndTime must be called after setStartTime", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } else { + elapsedTime = endTime - startTime - idleTime; + } + } + + /** + * Set idle time pause. + * For use by SampleResultConverter/CSVSaveService. + * + * @param idle long + */ + public void setIdleTime(long idle) { + idleTime = idle; + } + + private void setTimes(long start, long end) { + setStartTime(start); + setEndTime(end); + } + + /** + * Record the start time of a sample + */ + public void sampleStart() { + if (startTime == 0) { + setStartTime(currentTimeInMillis()); + } else { + log.error("sampleStart called twice", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } + } + + /** + * Record the end time of a sample and calculate the elapsed time + */ + public void sampleEnd() { + if (endTime == 0) { + setEndTime(currentTimeInMillis()); + } else { + log.error("sampleEnd called twice", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } + } + + /** + * Pause a sample + */ + public void samplePause() { + if (pauseTime != 0) { + log.error("samplePause called twice", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } + pauseTime = currentTimeInMillis(); + } + + /** + * Resume a sample + */ + public void sampleResume() { + if (pauseTime == 0) { + log.error("sampleResume without samplePause", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } + idleTime += currentTimeInMillis() - pauseTime; + pauseTime = 0; + } + + /** + * When a Sampler is working as a monitor + * + * @param monitor flag whether this sampler is working as a monitor + * @deprecated since 3.2 NOOP + */ + @Deprecated + public void setMonitor(boolean monitor) { + // NOOP + } + + /** + * If the sampler is a monitor, method will return true. + * + * @return true if the sampler is a monitor + * @deprecated since 3.2 always return false + */ + @Deprecated + public boolean isMonitor() { + return false; + } + + /** + * The statistical sample sender aggregates several samples to save on + * transmission costs. + * + * @param count number of samples represented by this instance + */ + public void setSampleCount(int count) { + sampleCount = count; + } + + /** + * return the sample count. by default, the value is 1. + * + * @return the sample count + */ + public int getSampleCount() { + return sampleCount; + } + + /** + * Returns the count of errors. + * + * @return 0 - or 1 if the sample failed + *

+ * TODO do we need allow for nested samples? + */ + public int getErrorCount() { + return success ? 0 : 1; + } + + public void setErrorCount(int i) {// for reading from CSV files + // ignored currently + } + + /* + * TODO: error counting needs to be sorted out. + * + * At present the Statistical Sampler tracks errors separately + * It would make sense to move the error count here, but this would + * mean lots of changes. + * It's also tricky maintaining the count - it can't just be incremented/decremented + * when the success flag is set as this may be done multiple times. + * The work-round for now is to do the work in the StatisticalSampleResult, + * which overrides this method. + * Note that some JMS samplers also create samples with > 1 sample count + * Also the Transaction Controller probably needs to be changed to do + * proper sample and error accounting. + * The purpose of this work-round is to allow at least minimal support for + * errors in remote statistical batch mode. + * + */ + + /** + * In the event the sampler does want to pass back the actual contents, we + * still want to calculate the throughput. The bytes are the bytes of the + * response data. + * + * @param length the number of bytes of the response data for this sample + */ + public void setBytes(long length) { + bytes = length; + } + + /** + * In the event the sampler does want to pass back the actual contents, we + * still want to calculate the throughput. The bytes are the bytes of the + * response data. + * + * @param length the number of bytes of the response data for this sample + * @deprecated use setBytes(long) + */ + @Deprecated + public void setBytes(int length) { + setBytes((long) length); + } + + /** + * @param sentBytesCount long sent bytes + */ + public void setSentBytes(long sentBytesCount) { + sentBytes = sentBytesCount; + } + + /** + * @return the sentBytes + */ + public long getSentBytes() { + return sentBytes; + } + + /** + * return the bytes returned by the response. + * + * @return byte count + * @deprecated use getBytesAsLong + */ + @Deprecated + public int getBytes() { + return (int) getBytesAsLong(); + } + + /** + * return the bytes returned by the response. + * + * @return byte count + */ + public long getBytesAsLong() { + long tmpSum = this.getHeadersSize() + this.getBodySizeAsLong(); + return tmpSum == 0 ? bytes : tmpSum; + } + + /** + * @return Returns the latency. + */ + public long getLatency() { + return latency; + } + + /** + * Set the time to the first response + */ + public void latencyEnd() { + latency = currentTimeInMillis() - startTime - idleTime; + } + + /** + * This is only intended for use by SampleResultConverter! + * + * @param latency The latency to set. + */ + public void setLatency(long latency) { + this.latency = latency; + } + + /** + * @return Returns the connect time. + */ + public long getConnectTime() { + return connectTime; + } + + /** + * Set the time to the end of connecting + */ + public void connectEnd() { + connectTime = currentTimeInMillis() - startTime - idleTime; + } + + /** + * This is only intended for use by SampleResultConverter! + * + * @param time The connect time to set. + */ + public void setConnectTime(long time) { + this.connectTime = time; + } + + /** + * This is only intended for use by SampleResultConverter! + * + * @param timeStamp The timeStamp to set. + */ + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + + public void setURL(URL location) { + this.location = location; + } + + public URL getURL() { + return location; + } + + /** + * Get a String representation of the URL (if defined). + * + * @return ExternalForm of URL, or empty string if url is null + */ + public String getUrlAsString() { + return location == null ? "" : location.toExternalForm(); + } + + /** + * @return Returns the parent. + */ + public SampleResult getParent() { + return parent; + } + + /** + * @param parent The parent to set. + */ + public void setParent(SampleResult parent) { + this.parent = parent; + } + + public String getResultFileName() { + return resultFileName; + } + + public void setResultFileName(String resultFileName) { + this.resultFileName = resultFileName; + } + + public int getGroupThreads() { + return groupThreads; + } + + public void setGroupThreads(int n) { + this.groupThreads = n; + } + + public int getAllThreads() { + return allThreads; + } + + public void setAllThreads(int n) { + this.allThreads = n; + } + + // Bug 47394 + + /** + * Allow custom SampleSenders to drop unwanted assertionResults + */ + public void removeAssertionResults() { + this.assertionResults = null; + } + + /** + * Allow custom SampleSenders to drop unwanted subResults + */ + public void removeSubResults() { + this.subResults = null; + } + + /** + * Set the headers size in bytes + * + * @param size the number of bytes of the header + */ + public void setHeadersSize(int size) { + this.headersSize = size; + } + + /** + * Get the headers size in bytes + * + * @return the headers size + */ + public int getHeadersSize() { + return headersSize; + } + + /** + * @return the body size in bytes + * @deprecated replaced by getBodySizeAsLong() + */ + @Deprecated + public int getBodySize() { + return (int) getBodySizeAsLong(); + } + + /** + * @return the body size in bytes + */ + public long getBodySizeAsLong() { + return bodySize == 0 ? responseData.length : bodySize; + } + + /** + * @param bodySize the body size to set + */ + public void setBodySize(long bodySize) { + this.bodySize = bodySize; + } + + /** + * @param bodySize the body size to set + * @deprecated use setBodySize(long) + */ + @Deprecated + public void setBodySize(int bodySize) { + this.bodySize = bodySize; + } + + private static class NanoOffset extends Thread { + + private static volatile long nanoOffset; + + static long getNanoOffset() { + return nanoOffset; + } + + @Override + public void run() { + // Wait longer than a clock pulse (generally 10-15ms) + getOffset(30L); // Catch an early clock pulse to reduce slop. + while (true) { + getOffset(NANOTHREAD_SLEEP); // Can now afford to wait a bit longer between checks + } + } + + private static void getOffset(long wait) { + try { + TimeUnit.MILLISECONDS.sleep(wait); + long clock = System.currentTimeMillis(); + long nano = SampleResult.sampleNsClockInMs(); + nanoOffset = clock - nano; + } catch (InterruptedException ignore) { + // ignored + Thread.currentThread().interrupt(); + } + } + } + + /** + * @return the startNextThreadLoop + * @deprecated use {@link SampleResult#getTestLogicalAction()} + */ + @Deprecated + public boolean isStartNextThreadLoop() { + return testLogicalAction == TestLogicalAction.START_NEXT_ITERATION_OF_THREAD; + } + + /** + * @param startNextThreadLoop the startNextLoop to set + * @deprecated use SampleResult#setTestLogicalAction(TestLogicalAction) + */ + @Deprecated + public void setStartNextThreadLoop(boolean startNextThreadLoop) { + if (startNextThreadLoop) { + testLogicalAction = TestLogicalAction.START_NEXT_ITERATION_OF_THREAD; + } else { + testLogicalAction = TestLogicalAction.CONTINUE; + } + } + + /** + * Clean up cached data + */ + public void cleanAfterSample() { + this.responseDataAsString = null; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new IllegalStateException("This should not happen"); + } + } + + @Override + public List getSearchableTokens() throws Exception { + List datasToSearch = new ArrayList<>(4); + datasToSearch.add(getSampleLabel()); + datasToSearch.add(getResponseDataAsString()); + datasToSearch.add(getRequestHeaders()); + datasToSearch.add(getResponseHeaders()); + return datasToSearch; + } + + /** + * @return boolean true if this SampleResult should not be sent to Listeners + */ + public boolean isIgnore() { + return ignore; + } + + /** + * Call this method to tell JMeter to ignore this SampleResult by Listeners + */ + public void setIgnore() { + this.ignore = true; + } + + /** + * @return String first non null assertion failure message if assertionResults is not null, null otherwise + */ + public String getFirstAssertionFailureMessage() { + String message = null; + AssertionResult[] results = getAssertionResults(); + + if (results != null) { + // Find the first non-null message + for (AssertionResult result : results) { + message = result.getFailureMessage(); + if (message != null) { + break; + } + } + } + return message; + } + + /** + * @return the testLogicalAction + */ + public TestLogicalAction getTestLogicalAction() { + return testLogicalAction; + } + + /** + * @param testLogicalAction the testLogicalAction to set + */ + public void setTestLogicalAction(TestLogicalAction testLogicalAction) { + this.testLogicalAction = testLogicalAction; + } + + public void addVars(JMeterVariables vars) { + this.vars = vars; + } + + public JMeterVariables getVars() { + return this.vars; + } +} diff --git a/frontend/src/business/components/api/report/components/ResponseText.vue b/frontend/src/business/components/api/report/components/ResponseText.vue index 6dc3c409ac..266334cf73 100644 --- a/frontend/src/business/components/api/report/components/ResponseText.vue +++ b/frontend/src/business/components/api/report/components/ResponseText.vue @@ -22,6 +22,10 @@ + +

{{response.vars}}
+ +