/*
* -----------------------------------------------------------------------\
* PerfCake
*
* Copyright (C) 2010 - 2016 the original author or authors.
*
* 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 org.perfcake;
import org.perfcake.scenario.ReplayResults;
import org.perfcake.scenario.Scenario;
import org.perfcake.scenario.ScenarioLoader;
import org.perfcake.util.TimerBenchmark;
import org.perfcake.util.Utils;
import org.perfcake.validation.ValidationException;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.stream.Collectors;
/**
* Parses command line parameters, loads the scenario from XML or DSL file and executes it.
*
* @author <a href="mailto:pavel.macik@gmail.com">Pavel Macík</a>
* @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a>
*/
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "DM_EXIT", justification = "This class is allowed to terminate the JVM.")
public class ScenarioExecution {
/**
* Logger.
*/
private static final Logger log = LogManager.getLogger(ScenarioExecution.class);
/**
* Command line parameters.
*/
private CommandLine commandLine;
/**
* The scenario created from the specified XML file.
*/
private Scenario scenario;
/**
* Parses command line arguments and creates this class to take care of the Scenario execution.
*
* @param args
* command line arguments.
*/
private ScenarioExecution(final String[] args) {
Utils.initTimeStamps();
parseCommandLine(args);
// now it is safe to greet the user
log.info(String.format(PerfCakeConst.WELCOME, PerfCakeConst.VERSION));
Utils.initDebugAgent();
loadScenario();
}
/**
* Creates an instance of {@link org.perfcake.ScenarioExecution} and executes the scenario.
*
* @param args
* Command line arguments.
*/
public static void main(final String[] args) {
final ScenarioExecution se = new ScenarioExecution(args);
// Print system properties
se.printTraceInformation();
if (Utils.getProperty(PerfCakeConst.REPLAY_PROPERTY) == null) {
se.executeScenario();
} else {
se.replayScenario();
}
log.info("=== Goodbye! ===");
}
/**
* Executes the given scenario in the same way as PerfCake was started from the command line.
* It just allows easy usage in TestNG and jUnit frameworks for instance.
*
* @param scenarioFile
* The file with scenario definition.
* @param properties
* Any additional properties that would be normally set using -Dprop=value (most command line arguments can be set like this).
* @throws PerfCakeException
* When it was not possible to load the scenario.
*/
public static void execute(final String scenarioFile, final Properties properties) throws PerfCakeException {
final Properties backup = new Properties();
properties.forEach((k, v) -> {
if (System.getProperty(k.toString()) != null) {
backup.setProperty(k.toString(), System.getProperty(k.toString()));
}
System.setProperty(k.toString(), v.toString());
});
Utils.initTimeStamps();
Utils.initDebugAgent();
final Scenario scenario = ScenarioLoader.load(scenarioFile);
scenario.init();
scenario.run();
scenario.close();
backup.forEach((k, v) -> System.setProperty(k.toString(), v.toString()));
}
/**
* Parses a single command line parameter/option.
*
* @param option
* The parameter/option name.
* @param property
* The system property to which the option value should be stored.
* @param defaultValue
* The default value for the option when it is not present at the command line.
*/
private void parseParameter(final String option, final String property, final String defaultValue) {
if (commandLine.hasOption(option)) {
System.setProperty(property, commandLine.getOptionValue(option));
} else {
if (defaultValue != null) {
System.setProperty(property, defaultValue);
}
}
}
/**
* Parses additional user properties specified in a property file and at the command line.
*/
private void parseUserProperties() {
final Properties props = new Properties();
final String propsFile = System.getProperty(PerfCakeConst.PROPERTIES_FILE_PROPERTY);
if (propsFile != null) {
try (final FileInputStream propsInputStream = new FileInputStream(propsFile)) {
props.load(propsInputStream);
} catch (final IOException e) {
// we can still continue without reading file
log.warn(String.format("Unable to read the properties file '%s': ", propsFile), e);
}
}
props.putAll(commandLine.getOptionProperties("D"));
for (final Entry<Object, Object> entry : props.entrySet()) {
System.setProperty(entry.getKey().toString(), entry.getValue().toString());
}
}
/**
* Parses the command line.
*
* @param args
* Command line arguments.
*/
@SuppressWarnings("static-access")
private void parseCommandLine(final String[] args) {
final HelpFormatter formatter = new HelpFormatter();
final Options options = new Options();
options.addOption(Option.builder("h").longOpt(PerfCakeConst.HELP_OPT).desc("prints help/usage").build());
options.addOption(Option.builder("s").longOpt(PerfCakeConst.SCENARIO_OPT).desc("scenario to be executed").hasArg().argName("SCENARIO").build());
options.addOption(Option.builder("sd").longOpt(PerfCakeConst.SCENARIOS_DIR_OPT).desc("directory for scenarios").hasArg().argName("SCENARIOS_DIR").build());
options.addOption(Option.builder("md").longOpt(PerfCakeConst.MESSAGES_DIR_OPT).desc("directory for messages").hasArg().argName("MESSAGES_DIR").build());
options.addOption(Option.builder("pd").longOpt(PerfCakeConst.PLUGINS_DIR_OPT).desc("directory for plugins").hasArg().argName("PLUGINS_DIR").build());
options.addOption(Option.builder("pf").longOpt(PerfCakeConst.PROPERTIES_FILE_OPT).desc("custom system properties file").hasArg().argName("PROPERTIES_FILE").build());
options.addOption(Option.builder("log").longOpt(PerfCakeConst.LOGGING_LEVEL_OPT).desc("logging level").hasArg().argName("LOG_LEVEL").build());
options.addOption(Option.builder("r").longOpt(PerfCakeConst.REPLAY_OPT).desc("raw file to be replayed").hasArg().argName("RAW_FILE").build());
options.addOption(Option.builder("d").longOpt(PerfCakeConst.DEBUG_OPT).desc("start debug JMX agent for external monitoring").build());
options.addOption(Option.builder("dn").longOpt(PerfCakeConst.DEBUG_AGENT_NAME_OPT).desc("debug agent name in the JMX tree").hasArg().argName("AGENT_NAME").build());
options.addOption(Option.builder("skip").longOpt(PerfCakeConst.SKIP_TIMER_BENCHMARK_OPT).desc("skip system timer benchmark").build());
options.addOption(Option.builder("D").argName("property=value").numberOfArgs(2).valueSeparator().desc("system properties").build());
final CommandLineParser commandLineParser = new DefaultParser();
try {
commandLine = commandLineParser.parse(options, args);
} catch (final ParseException pe) {
log.fatal("Cannot parse application parameters: ", pe);
formatter.printHelp(PerfCakeConst.USAGE_HELP, options);
System.exit(PerfCakeConst.ERR_PARAMETERS);
return;
}
if (commandLine.hasOption(PerfCakeConst.HELP_OPT)) {
System.out.println(String.format(PerfCakeConst.WELCOME, PerfCakeConst.VERSION));
formatter.printHelp(PerfCakeConst.USAGE_HELP, options);
System.exit(PerfCakeConst.ERR_PRINT_HELP);
return;
}
if (commandLine.hasOption(PerfCakeConst.SCENARIO_OPT)) {
System.setProperty(PerfCakeConst.SCENARIO_PROPERTY, commandLine.getOptionValue(PerfCakeConst.SCENARIO_OPT));
} else {
formatter.printHelp(PerfCakeConst.USAGE_HELP, options);
System.exit(PerfCakeConst.ERR_NO_SCENARIO);
return;
}
if (commandLine.hasOption(PerfCakeConst.SKIP_TIMER_BENCHMARK_OPT)) {
System.setProperty(PerfCakeConst.SKIP_TIMER_BENCHMARK_PROPERTY, "true");
}
if (commandLine.hasOption(PerfCakeConst.DEBUG_OPT)) {
System.setProperty(PerfCakeConst.DEBUG_PROPERTY, "true");
}
parseParameter(PerfCakeConst.SCENARIOS_DIR_OPT, PerfCakeConst.SCENARIOS_DIR_PROPERTY, Utils.determineDefaultLocation("scenarios"));
parseParameter(PerfCakeConst.MESSAGES_DIR_OPT, PerfCakeConst.MESSAGES_DIR_PROPERTY, Utils.determineDefaultLocation("messages"));
parseParameter(PerfCakeConst.PLUGINS_DIR_OPT, PerfCakeConst.PLUGINS_DIR_PROPERTY, Utils.DEFAULT_PLUGINS_DIR.getAbsolutePath());
parseParameter(PerfCakeConst.PROPERTIES_FILE_OPT, PerfCakeConst.PROPERTIES_FILE_PROPERTY, null);
parseParameter(PerfCakeConst.LOGGING_LEVEL_OPT, PerfCakeConst.LOGGING_LEVEL_PROPERTY, null);
parseParameter(PerfCakeConst.REPLAY_OPT, PerfCakeConst.REPLAY_PROPERTY, null);
parseParameter(PerfCakeConst.DEBUG_AGENT_NAME_OPT, PerfCakeConst.DEBUG_AGENT_NAME_PROPERTY, PerfCakeConst.DEBUG_AGENT_DEFAULT_NAME);
if (Utils.getProperty(PerfCakeConst.LOGGING_LEVEL_PROPERTY, null) != null) {
Utils.setLoggingLevel(Level.toLevel(Utils.getProperty(PerfCakeConst.LOGGING_LEVEL_PROPERTY), Level.INFO));
}
parseUserProperties();
if (System.getProperty(PerfCakeConst.KEYSTORES_DIR_PROPERTY) == null) {
System.setProperty(PerfCakeConst.KEYSTORES_DIR_PROPERTY, Utils.determineDefaultLocation("keystores"));
}
}
/**
* Prints trace information for test debugging purposes.
*/
private void printTraceInformation() {
if (log.isTraceEnabled()) {
log.trace("System properties:");
final List<String> p = System.getProperties().entrySet().stream().map(entry -> "\t" + entry.getKey() + "=" + entry.getValue()).collect(Collectors.toCollection(() -> new LinkedList<>()));
Collections.sort(p);
for (final String s : p) {
log.trace(s);
}
// Print classpath
log.trace("Classpath:");
final ClassLoader currentClassLoader = ScenarioExecution.class.getClassLoader();
final URL[] curls = ((URLClassLoader) currentClassLoader).getURLs();
for (final URL curl : curls) {
log.trace("\t" + curl);
}
}
}
/**
* Loads the scenario from the XML file specified at the command line.
*/
private void loadScenario() {
final String scenarioFile = Utils.getProperty(PerfCakeConst.SCENARIO_PROPERTY);
try {
scenario = ScenarioLoader.load(scenarioFile);
} catch (final Exception e) {
log.fatal(String.format("Cannot load scenario '%s': ", scenarioFile), e);
System.exit(PerfCakeConst.ERR_SCENARIO_LOADING);
}
}
/**
* Executes the loaded scenario.
*/
private void executeScenario() {
if (Utils.getProperty(PerfCakeConst.SKIP_TIMER_BENCHMARK_PROPERTY) == null) {
TimerBenchmark.measureTimerResolution();
}
boolean err = false;
try {
scenario.init();
scenario.run();
} catch (final PerfCakeException e) {
log.fatal("Error running scenario: ", e);
err = true;
} finally {
try {
scenario.close();
} catch (final ValidationException ve) {
log.warn(ve.getMessage());
System.exit(PerfCakeConst.ERR_VALIDATION);
} catch (final PerfCakeException e) {
log.fatal("Scenario did not finish properly: ", e);
err = true;
}
}
if (err) {
System.exit(PerfCakeConst.ERR_SCENARIO_EXECUTION);
}
if (!scenario.areAllThreadsTerminated()) {
log.warn("There are some blocked threads that were not possible to terminate. The test results might be flawed."
+ " This is usually caused by deadlocks or race conditions in the application under test.");
System.exit(PerfCakeConst.ERR_BLOCKED_THREADS);
}
}
/**
* Replays the previously recorded raw results.
*/
private void replayScenario() {
final String rawFile = Utils.getProperty(PerfCakeConst.REPLAY_PROPERTY);
log.info("Replaying raw results recorded in {}.", rawFile);
try (final ReplayResults replay = new ReplayResults(scenario, rawFile)) {
replay.replay();
} catch (IOException ioe) {
log.fatal("Unable to replay scenario: ", ioe);
System.exit(PerfCakeConst.ERR_SCENARIO_REPLAY);
}
}
}