
com.xceptance.xlt.agent.LoadTestRunner Maven / Gradle / Ivy
/*
* Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
*
* 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.xceptance.xlt.agent;
import java.lang.reflect.Method;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xceptance.xlt.agentcontroller.TestUserConfiguration;
import com.xceptance.xlt.agentcontroller.TestUserStatus;
import com.xceptance.xlt.api.engine.DataManager;
import com.xceptance.xlt.api.engine.GlobalClock;
import com.xceptance.xlt.engine.DataManagerImpl;
import com.xceptance.xlt.engine.SessionImpl;
import com.xceptance.xlt.engine.util.TimerUtils;
/**
* Load test runner.
*
* Group of autonomous threads that execute one or more tests according to their configuration.
*
*
* @author Jörg Werner (Xceptance Software Technologies GmbH)
*/
public class LoadTestRunner extends Thread
{
/**
* Class logger instance.
*/
private static final Logger log = LoggerFactory.getLogger(LoadTestRunner.class);
/**
* Configuration.
*/
private final TestUserConfiguration config;
/**
* Status of runner.
*/
private final TestUserStatus status;
/**
* Timer used for synchronization and test execution controlling.
*/
private AbstractExecutionTimer timer;
/**
* Some information about this and other agents.
*/
private final AgentInfo agentInfo;
/**
* Whether this runner has been aborted.
*/
private volatile boolean aborted;
/**
* Creates a new LoadTestRunner object for the given load test configuration. Typically, multiple runners are
* started for one test case configuration, so the number of the current runner is passed as well.
*
* @param config
* the load test configuration
* @param agentInfo
* load test agent information
* @param timer
* the execution timer that controls this load test runner
*/
public LoadTestRunner(final TestUserConfiguration config, final AgentInfo agentInfo, final AbstractExecutionTimer timer)
{
// create a new thread group for each LoadTestRunner as a means
// to keep the main thread and any supporting threads together
super(new ThreadGroup(config.getUserId()), config.getUserId());
this.config = config;
this.agentInfo = agentInfo;
this.timer = timer;
status = new TestUserStatus();
status.setUserName(config.getUserId());
}
/**
* Returns the current test case status.
*
* @return the status
*/
public TestUserStatus getTestUserStatus()
{
return status;
}
/**
* Runs the test case as configured in the test case configuration.
*/
@Override
public void run()
{
try
{
final long now = GlobalClock.get().millis();
// get and check the test case class
Class> testCaseClass = null;
try
{
// try to get the corresponding Java class
testCaseClass = Class.forName(config.getTestCaseClassName());
// class found -> validate it
checkTestCaseClass(testCaseClass);
}
// no such Java class
catch (final ClassNotFoundException cnf)
{
// failed to get resource URL -> indicate error
throw new RuntimeException("Could not find java class '" + config.getTestCaseClassName() + "'.");
}
// get the other test case parameters
final int iterations = config.getNumberOfIterations();
final int warmUpPeriod = config.getWarmUpPeriod();
final int measurementPeriod = config.getMeasurementPeriod();
final int initialDelay = config.getInitialDelay();
final int duration = warmUpPeriod + measurementPeriod;
// initialize the master controller status
status.setStartDate(now + initialDelay);
status.setEndDate(now + initialDelay + duration);
// initialize the session
final SessionImpl session = SessionImpl.getCurrent();
session.setUserCount(config.getNumberOfUsers());
session.setUserName(config.getUserName());
session.setUserNumber(config.getInstance());
session.setAbsoluteUserNumber(config.getAbsoluteUserNumber());
session.setTotalUserCount(config.getTotalUserCount());
session.setLoadTest(true);
session.setAgentID(agentInfo.getAgentID());
session.setAgentNumber(agentInfo.getAgentNumber());
session.setTotalAgentCount(agentInfo.getTotalAgentCount());
// set the logging window
final DataManager dataMgr = session.getDataManager();
dataMgr.setStartOfLoggingPeriod(now + initialDelay + warmUpPeriod);
dataMgr.setEndOfLoggingPeriod(now + initialDelay + warmUpPeriod + measurementPeriod);
// run the test
if (iterations != 0)
{
status.setMode(TestUserStatus.Mode.ITERATION);
runIterations(testCaseClass, iterations);
}
else
{
status.setMode(TestUserStatus.Mode.TIME_PERIOD);
if (duration != 0)
{
runDuration(testCaseClass, duration);
}
else
{
log.warn("Both number of iterations and computed duration are unspecified for test case: " +
config.getTestCaseClassName());
}
}
// set the final state
if (aborted)
{
status.setState(TestUserStatus.State.Aborted);
}
else
{
status.setPercentageComplete(100);
status.setState(TestUserStatus.State.Finished);
}
}
catch (final Exception ex)
{
log.error("Failed to run test as user: " + getName(), ex);
status.setState(TestUserStatus.State.Failed);
status.setException(ex);
}
}
/**
* Marks this load test runner as aborted.
*/
public void setAborted()
{
aborted = true;
}
/**
* Checks whether the test case class is acceptable, i.e., whether it has exactly one active test method. This
* means, there can be only one method in the class that is - at the same time - annotated with "@Test" and not
* annotated with "@Ignore". Furthermore, the class itself must not be annotated with "@Ignore"
*
* @param testCaseClass
* the test case class
* @throws RuntimeException
* if there is *not* exactly one active test method in this class
*/
private void checkTestCaseClass(final Class> testCaseClass)
{
// check whether the test case class is annotated with @Ignore
if (testCaseClass.isAnnotationPresent(Ignore.class))
{
throw new RuntimeException("Test class is annotated with @Ignore: " + testCaseClass.getName());
}
// check whether we have exactly one active test method
int testMethodCount = 0;
for (final Method method : testCaseClass.getMethods())
{
if (method.isAnnotationPresent(Test.class) && !method.isAnnotationPresent(Ignore.class))
{
testMethodCount++;
}
}
if (testMethodCount != 1)
{
throw new RuntimeException("No or more than one active test method found in class: " + testCaseClass.getName());
}
}
/**
* Runs the passed test case repeatedly until the specified number of iterations has been reached. The test case
* status is updated after each iteration.
*
* @param testCaseClass
* the test case to execute
* @param iterations
* the number of test iterations
*/
private void runIterations(final Class> testCaseClass, final int iterations)
{
final String testClassName = (testCaseClass == null ? config.getTestCaseClassName() : testCaseClass.toString());
log.info("Load test thread started (" + testClassName + " / " + iterations + " iterations)");
for (int i = 0; i < iterations; i++)
{
try
{
status.setState(TestUserStatus.State.Waiting);
timer.waitForNextExecution();
status.setState(TestUserStatus.State.Running);
runTestCase(testCaseClass, status);
}
catch (final InterruptedException ex)
{
break;
}
status.setPercentageComplete((i + 1) * 100 / iterations);
}
log.info("Load test thread finished.");
}
/**
* Runs the passed test case repeatedly until the specified number of milliseconds has passed. The test case status
* is updated after each iteration.
*
* @param testCaseClass
* the test case to execute
* @param duration
* the test period [ms]
*/
private void runDuration(final Class> testCaseClass, final int duration)
{
final String testClassName = (testCaseClass == null ? config.getTestCaseClassName() : testCaseClass.toString());
log.info("Load test thread started (" + testClassName + " / " + duration / 1000 + " s)");
final long startTime = GlobalClock.get().millis();
while (true)
{
try
{
status.setState(TestUserStatus.State.Waiting);
timer.waitForNextExecution();
status.setState(TestUserStatus.State.Running);
runTestCase(testCaseClass, status);
}
catch (final InterruptedException ex)
{
break;
}
final long now = GlobalClock.get().millis();
status.setPercentageComplete((int) ((now - startTime) * 100 / duration));
}
log.info("Load test thread finished.");
}
/**
* Runs the passed test case *once*. Depending on the test result, the test case status is updated accordingly
* (errors, iterations, runtime, etc.).
*
* @param testCaseClass
* the test case to execute
* @param status
* the test case status
*/
protected void runTestCase(final Class> testCaseClass, final TestUserStatus status) throws InterruptedException
{
// check whether we have to quit before attempting a new iteration
final SessionImpl session = SessionImpl.getCurrent();
if (session.wasMarkedAsExpired())
{
throw new InterruptedException("User aborted as the load test is over");
}
// remember start time
final long startTime = TimerUtils.get().getStartTime();
// make sure transaction data recording is initiated even if XltTestRunner is not used by the test
session.startTransaction();
// execute the test via JUnit
final Result result = new JUnitCore().run(testCaseClass);
// check again whether we have to quit before dealing with the results
if (session.wasMarkedAsExpired())
{
throw new InterruptedException("User aborted as the load test is over");
}
// reset the interrupted flag for a clean new transaction
Thread.interrupted();
// remember runtime
final long runTime = TimerUtils.get().getElapsedTime(startTime);
final boolean failed = !result.wasSuccessful();
Throwable failure = null;
if (failed)
{
failure = result.getFailures().get(0).getException();
if (log.isErrorEnabled())
{
log.error(String.format("Failure while executing test (user: '%s', output: '%s'):", session.getUserID(), session.getID()),
failure);
}
}
// maintain the master controller status
final long now = GlobalClock.get().millis();
final DataManagerImpl dataManager = session.getDataManager();
status.incrementIterations();
status.addToTotalRuntime(runTime);
status.setLastRuntime(runTime);
status.setElapsedTime(now - status.getStartDate());
status.setLastModifiedDate(now);
status.setEvents(dataManager.getNumberOfEvents());
if (failed)
{
status.incrementErrors();
}
// maintain the transaction statistics (if not already done)
if (session.isTransactionPending())
{
/*
* If transaction data recording is still in progress the test does not use XltTestRunner (which stops the
* transaction automatically when test method execution has finished). In this case, we cannot rely on the
* failure information tracked by the session itself but use the failure information reported by JUnit
* instead.
*/
// restore failure information at session...
session.setFailed(failed);
session.setFailReason(failure);
// and stop the transaction
session.stopTransaction();
}
// clean up
session.clearFailedActionName();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy