com.mark59.core.JmeterFunctionsImpl Maven / Gradle / Ivy
Show all versions of mark59-core Show documentation
/*
* Copyright 2019 Insurance Australia Group Limited
*
* Licensed 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 com.mark59.core;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import com.mark59.core.interfaces.JmeterFunctions;
import com.mark59.core.utils.Mark59Constants;
import com.mark59.core.utils.Mark59Constants.JMeterFileDatatypes;
/**
* Implements the JmeterFunctions interface, with methods that can be called throughout the life cycle of the test in order to handle
* behavior around transaction recording and timing.
*
* Typical usage from a scripting perspective is to time a transaction. For example:
*
* jm.startTransaction("DH-lifecycle-0100-deleteMultiplePolicies");
* deleteMultiplePoliciesPage.submit().submit();
* jm.endTransaction("DH-lifecycle-0100-deleteMultiplePolicies")
*
*
* where 'jm
' is this class, or an extension of this class (eg SeleniumAbstractJavaSamplerClient)
*
*
The class works by creating JMeter 'sub-results', one per recored transaction, which are attached to a main SampleResult.
* At the end of the script the sub-results ({@link #tearDown()} are printed (at LOG info level).
*
*
* @author Philip Webb
* @author Michael Cohen
* Written: Australian Winter 2019
*/
public class JmeterFunctionsImpl implements JmeterFunctions {
private static final Logger LOG = LogManager.getLogger(JmeterFunctionsImpl.class);
public static final String DATAPOINT = "DATAPOINT";
protected SampleResult mainResult = new SampleResult();
protected Map transactionMap = new ConcurrentHashMap<>();
protected String threadName;
protected boolean isForcedFail;
public JmeterFunctionsImpl(String threadName) {
this.threadName = threadName;
mainResult.sampleStart();
}
/**
* Called upon completion of the test run.
*
* Traverses the internal created transactionMap, looking for any transactions that had been
* started but not completed. If incomplete transactions are encountered then they are ended and flagged as
* "failed".
*
* If a test execution contains one or more failed transactions, the entire script
* run is flagged as a failed test.
*
* Once any outstanding transactions are completed, the SampleResult object is
* finalised (its status is set as PASS or FAIL).
*
*
* this.tearDown() is called as part of the framework, so an end user should not need to call this method
* themselves unless they're using a custom implementation of AbstractJmeterTestRunner.runTest(JavaSamplerContext)
*
*/
@Override
public void tearDown() {
for (Entry subResultEntry : transactionMap.entrySet()) {
if (StringUtils.isBlank(subResultEntry.getValue().getResponseMessage())){
endTransaction(subResultEntry.getValue().getSampleLabel(), Outcome.FAIL);
}
}
if (allSamplesPassed() && !isForcedFail){
tearDownMainResult(Outcome.PASS);
} else {
tearDownMainResult(Outcome.FAIL);
}
logThreadTransactionResults();
}
private boolean allSamplesPassed() {
return Arrays.asList(mainResult.getSubResults()).stream().noneMatch(sr->!sr.isSuccessful());
}
/**
* Completes the test main transaction - expected to be invoked at end of test script run.
* Note from Jmeter 5.0 a call to set the end time of the main transaction is called as each
* sub-result ends, so a call to the sampleEnd() method only needs to be made if no subResult
* has already set the main transaction end time
*
* A data type of 'PARENT' is used to indicate this is a main result (normally expected to have sub-results)
* produced using the mark59 framework is set. This is useful to to separate results from sub-results, particularly
* for JMeter result files in CSV format, as a CSV has a flat structure.
*
*/
private void tearDownMainResult(Outcome outcome) {
if (mainResult.getEndTime() == 0) {
mainResult.sampleEnd(); // stop stopwatch
}
mainResult.setSuccessful(outcome.isOutcomeSuccess());
mainResult.setResponseMessage(outcome.getOutcomeText());
mainResult.setResponseCode(outcome.getOutcomeResponseCode()); // 200 code
mainResult.setDataType(JMeterFileDatatypes.PARENT.getDatatypeText() );
}
/**
* Starts timing a transaction. Note cannot start a transaction using the same name as one already running
* in a script (controlled using an internally created transactionMap holding a key of running transaction names)
* and starts timing the transaction.
*
* @param transactionLabel ('label' in JMeter terminology) for the transaction
* @throws IllegalArgumentException if the transaction name supplied is an illegal value (null or empty) or already in use.
*/
@Override
public void startTransaction(String transactionLabel) {
if (StringUtils.isBlank(transactionLabel)) {
throw new IllegalArgumentException("transactionLabel cannot be null or empty");
}
if (transactionMap.containsKey(transactionLabel)) {
throw new IllegalArgumentException("Error - a transaction using the passed transaction name appears to be currently"
+ " in use (running) in this script : " + transactionLabel);
}
SampleResult sampleResult = new SampleResult();
sampleResult.setSampleLabel(transactionLabel);
transactionMap.put(transactionLabel, sampleResult);
sampleResult.sampleStart();
}
/**
* Ends an existing transaction (SampleResult), stopping the running timer.
*
* When the transaction is added to the main result (with a status of a 'passed')
* Elapsed time is recorded in milliseconds, the standard for JMeter
* Once a transaction is ended it is added to the main SampleResult object that will ultimately be returned upon script completion.
*
Once a transaction is ended, an internal key entry for the transaction ('label' in JMeter terminology) is cleared, freeing the
* transaction name to be re-used if desired.
*
* @param transactionLabel label for the transaction
* @throws IllegalArgumentException if the transactionLabel supplied is an illegal value (null or empty)
* @throws NoSuchElementException if the transactionLabel doesn't exist in the transactionMap
* @return the JMeter subresult for this transaction - which includes the transaction time (getTime) *
*/
@Override
public SampleResult endTransaction(String transactionLabel) {
return endTransaction(transactionLabel, Outcome.PASS, null);
}
/**
* Ends an existing transaction (SampleResult), stopping the running timer.
*
* When the transaction is added to the main result, it will be given a success state based on the Outcome passed in
* Elapsed time is recorded in milliseconds, the standard for JMeter
* Once a transaction is ended it is added to the main SampleResult object that will ultimately be returned upon test completion.
*
Once the transaction is ended, an internal key entry for the transaction ('label' in JMeter terminology) is cleared, freeing the
* transaction name to be re-used if desired.
*
* @param transactionLabel label for the transaction
* @param result the success or failure state of the transaction
*
* @throws IllegalArgumentException if the transactionLabel supplied is an illegal value (null or empty)
* @throws NoSuchElementException if the transactionLabel doesn't exist in the transactionMap
* @return the JMeter sub-result for this transaction (which includes the transaction time)
*/
public SampleResult endTransaction(String transactionLabel, Outcome result) {
return endTransaction(transactionLabel, result, null);
}
/**
* Ends an existing transaction (SampleResult), stopping the running timer.
*
* When the transaction is added to the main result, it will be given a success state based on the Outcome passed in
* Elapsed time is recorded in milliseconds, the standard for JMeter
* Once a transaction is ended it is added to the main SampleResult object that will ultimately be returned upon test completion.
*
Once a transaction is ended, an internal key entry for the transaction ('label' in JMeter terminology) is cleared, freeing the
* transaction name to be re-used if desired.
*
* Allows for a response message (which can be printed in a JMeter report for errored transactions). This will
* default to "200" or "-1" for passed/failed transaction if a blank or null message is passed.
*
* @param transactionLabel label for the transaction
* @param result the success or failure state of the transaction
* @param responseCode response message (useful for error transactions)
*
* @throws IllegalArgumentException if the transactionLabel supplied is an illegal value (null or empty)
* @throws NoSuchElementException if the transactionLabel doesn't exist in the transactionMap
* @return the JMeter subresult for this transaction - which includes the transaction time (getTime)
*/
public SampleResult endTransaction(String transactionLabel, Outcome result, String responseCode) {
if (StringUtils.isBlank(transactionLabel))
throw new IllegalArgumentException("transactionLabel cannot be null or empty");
if (!transactionMap.containsKey(transactionLabel))
throw new NoSuchElementException(
"could not find SampleResult in transactionMap as it does not contain a key matching the expected value : "
+ transactionLabel);
if (StringUtils.isBlank(responseCode))
responseCode = result.getOutcomeResponseCode();
SampleResult subResult = transactionMap.get(transactionLabel);
subResult.sampleEnd();
subResult.setSuccessful(result.isOutcomeSuccess());
subResult.setResponseMessage(result.getOutcomeText());
subResult.setResponseCode(responseCode); // 200 | -1 | responseCode (passed string)
subResult.setSampleLabel(transactionLabel);
mainResult.addSubResult(subResult, false); // prevents strange indexed named transactions (from Jmeter 5.0)
transactionMap.remove(transactionLabel);
return subResult;
}
/**
* Adds a new SubResult to the main result, bypassing the transactionMap used by the timing methods:
* this.startTransaction(String); this.endTransaction(String).
*
*
The new SubResult must be given both a transactionLabel and a transactionTime, transactionTime
* is expected to be in milliseconds.
*
* @param transactionLabel label for the transaction
* @param transactionTime time taken for the transaction. Expects Milliseconds.
*
* @throws IllegalArgumentException if the transactionLabel is null or empty
* @return SampleResult
*/
@Override
public SampleResult setTransaction(String transactionLabel, long transactionTime){
return setTransaction(transactionLabel, transactionTime, true);
}
/**
* Adds a new SubResult directly to the main result, without the need to use timing methods
* startTransaction(txnName) and endTransaction(txnName).
*
*
When the transaction is added to the main result, it will be given a success state based on the Outcome passed in
*
* The new SubResult must be given both a transactionLabel and a transactionTime, transactionTime
* is expected to be in milliseconds.
*
* @param transactionLabel label for the transaction
* @param transactionTime time taken for the transaction. Expects Milliseconds.
*
* @throws IllegalArgumentException if the transactionLabel is null or empty
* @return SampleResult
*/
@Override
public SampleResult setTransaction(String transactionLabel, long transactionTime, boolean success) {
return createSubResult(transactionLabel, transactionTime, success ? Outcome.PASS : Outcome.FAIL, JMeterFileDatatypes.TRANSACTION, null );
}
/**
* Adds a new SubResult directly to the main result, without the need to use timing methods
* startTransaction(txnName) and endTransaction(txnName).
*
*
When the transaction is added to the main result, it will be given a success state based on the Outcome passed in
*
* The new SubResult must be given both a transactionLabel and a transactionTime, transactionTime
* is expected to be in milliseconds.
*
*
Allows for a response message (which can be printed in a JMeter report for errored transactions). This will
* default to "200" or "-1" for passed/failed transaction if a blank or null message is passed.
*
* @param transactionLabel label for the transaction
* @param transactionTime time taken for the transaction
* @param success the success (true) or failure (false) state of the transaction
* @param responseCode response message (useful for error transactions)
*
* @throws IllegalArgumentException if the transactionLabel is null or empty
* @return SampleResult
*/
public SampleResult setTransaction(String transactionLabel, long transactionTime, boolean success, String responseCode) {
return createSubResult(transactionLabel, transactionTime, success ? Outcome.PASS : Outcome.FAIL, JMeterFileDatatypes.TRANSACTION, responseCode );
}
/**
* Similar to this.{@link #userDataPoint(String, long)}, but instead of just being able to create a JMeter sub-result of
* data type DATAPOINT, you can specify the type from the list of allowed types used by Mark59 as defined by
* {@link Mark59Constants.JMeterFileDatatypes}
*
*
For example
*
* jm.userDatatypeEntry("MyServersCPUUtilizationPercent", 40L, Mark59Constants.JMeterFileDatatypes.CPU_UTIL );
*
* @return SampleResult
*/
@Override
public SampleResult userDatatypeEntry(String dataPointName, long dataPointValue, JMeterFileDatatypes outputDatatype) {
if (LOG.isDebugEnabled()) LOG.debug(" userDatatypeEntry Name:Value [ " + dataPointName + ":" + dataPointValue + ":" + outputDatatype.getDatatypeText() + "]");
return createSubResult(dataPointName, dataPointValue, Outcome.PASS, outputDatatype, null);
}
/**
* Adds a new SubResult to the main result reflecting a non-timing related metric - a 'DATAPOINT'.
* A DATAPOINT must be given both a dataPointName and a dataPointValue, the value being
* any arbitrary long value.
*
* @param dataPointName label for the DATAPOINT
* @param dataPointValue an arbitrary non-timing metric
*
* @throws IllegalArgumentException if the dataPointName is null or empty
* @return SampleResult
*/
@Override
public SampleResult userDataPoint(String dataPointName, long dataPointValue ) {
if (LOG.isDebugEnabled()) LOG.debug(" userDataPoint Name:Value [ " + dataPointName + ":" + dataPointValue + "]");
return createSubResult(dataPointName, dataPointValue, Outcome.PASS, JMeterFileDatatypes.DATAPOINT, null);
}
private SampleResult createSubResult(String dataPointName, long dataPointValue, Outcome result, JMeterFileDatatypes jmeterFileDatatypes, String responseCode) {
if (StringUtils.isBlank(dataPointName))
throw new IllegalArgumentException("dataPointName cannot be null or empty");
if (StringUtils.isBlank(responseCode))
responseCode = result.getOutcomeResponseCode();
SampleResult subResult = new SampleResult();
subResult.setSuccessful(result.isOutcomeSuccess()); // true | false
subResult.setResponseCode(responseCode); // 200 | -1 | responseCode (passed string)
subResult.setResponseMessage(result.getOutcomeText()); // PASS | FAIL
subResult.setDataType(jmeterFileDatatypes.getDatatypeText() );
subResult.setSampleLabel(dataPointName);
subResult.sampleStart();
subResult.setEndTime(subResult.getStartTime() + dataPointValue );
mainResult.addSubResult(subResult, false);
return subResult;
}
/**
* Return results from running the test. Specifically for the Mark59 implementation,
* it can be used to access the transaction results in a running script by
* getting the subResults list.
*/
@Override
public SampleResult getMainResult() {
return mainResult;
}
/**
* Fetches the SampleResult from the transactionMap that matches the supplied label.
* The transactionMap is primarily intended as an internal key tracking mechanism to prevent multiple
* transactions with the same name being started and running concurrently within the one script
*
* If it fails to find a SampleResult it either means no such label had been added to the transactionMap,
* or the SampleResult has already been finalized and added to the main result.
*
* @param label the transaction label for the SampleResult to locate.
* @return SampleResult belonging to the supplied label.
*/
public SampleResult getSampleResultWithLabel(String label) {
return transactionMap.get(label);
}
/**
* Searches the main result for all instances of the supplied label, collating the SampleResults into a List and returning all of them.
* Note: primary purpose is as a helper method for junit testing.
*
* @param label the transaction label for the SampleResults to locate.
* @return a list of sample results
*/
public List getSampleResultFromMainResultWithLabel(String label) {
return Arrays.asList(mainResult.getSubResults()).stream()
.filter(sr -> sr.getSampleLabel().equals(label))
.collect(Collectors.toList());
}
private void logThreadTransactionResults() {
SampleResult[] sampleResult = mainResult.getSubResults();
LOG.info("");
LOG.info(Thread.currentThread().getName() + " result (" + mainResult.getResponseMessage() + ")" ) ;
LOG.info(String.format("%-40s%-10s%-60s%-20s%-20s", "Thread", "#", "txn name", "Resp Message", "resp time"));
for (int i = 0; i < sampleResult.length; i++) {
SampleResult subSR = sampleResult[i];
if (StringUtils.isBlank(subSR.getDataType())) {
LOG.info(String.format("%-40s%-10s%-60s%-20s%-20s", threadName, i, subSR.getSampleLabel(),
subSR.getResponseMessage(), subSR.getTime()));
} else {
LOG.info(String.format("%-40s%-10s%-60s%-20s%-20s", threadName, i, subSR.getSampleLabel(),
subSR.getResponseMessage() + " (" + subSR.getDataType() + ")" , subSR.getTime()));
}
}
LOG.info("");
}
/**
* Called to set the main result of a test to a failed state, regardless of the state of the sub results attached to the main result.
* Normally, a main result would only fail if at least one of it's sub results was a fail.
*/
@Override
public void failTest() {
isForcedFail = true;
}
}