diff --git a/config/reportgenerator.properties b/config/reportgenerator.properties index 4c9a4a945..441a45088 100644 --- a/config/reportgenerator.properties +++ b/config/reportgenerator.properties @@ -118,8 +118,49 @@ com.xceptance.xlt.reportgenerator.charts.height = 300 #com.xceptance.xlt.reportgenerator.charts.cappingMode = always -## The percentage of values taken when calculating the moving average series. -com.xceptance.xlt.reportgenerator.charts.movingAverage.percentageOfValues = 5 +############################################################################### +# +# Moving Average Configuration +# +# This describes the configuration of the common moving average series. It is also possible to configure up to 5 +# additional moving averages that will be displayed in the average charts. +# +# Format: +# +# com.xceptance.xlt.reportgenerator.charts.commonAverage.type = +# com.xceptance.xlt.reportgenerator.charts.commonAverage.value = +# +# com.xceptance.xlt.reportgenerator.charts.averages..type = +# com.xceptance.xlt.reportgenerator.charts.averages..value = +# +# +# type ......... The type of moving average to calculate (allowed values are "percentage" and "time"). +# +# value ........ The value for calculating the moving average. Value format must match the selected type. Percentage +# values can be provided as integer values (optionally ending with "%"). Time values can be provided +# in the usual allowed time formats (see examples below). Values must be greater than "0" and +# percentage values must not exceed 100%. +# Allowed percentage values are for example: 5%, 12 +# Allowed time values are for example: 30s, 1m15s, 2:30, 45 +# +# index ........ Numeric index for the additional moving averages (allowed values are "1" to "5"). +# +# Example configuration: +# com.xceptance.xlt.reportgenerator.charts.commonAverage.type = percentage +# com.xceptance.xlt.reportgenerator.charts.commonAverage.value = 5% +# com.xceptance.xlt.reportgenerator.charts.averages.1.type = time +# com.xceptance.xlt.reportgenerator.charts.averages.1.value = 1m30s +# +############################################################################### + +com.xceptance.xlt.reportgenerator.charts.commonAverage.type = percentage +com.xceptance.xlt.reportgenerator.charts.commonAverage.value = 5% + +com.xceptance.xlt.reportgenerator.charts.averages.1.type = percentage +com.xceptance.xlt.reportgenerator.charts.averages.1.value = 2% + +com.xceptance.xlt.reportgenerator.charts.averages.2.type = percentage +com.xceptance.xlt.reportgenerator.charts.averages.2.value = 10% ## Whether dynamic/interactive charts are enabled in addition to the regular ## WEBP image charts (default: true). diff --git a/doc/internal-doc/api.sig b/doc/internal-doc/api.sig index fade27d86..928a5417f 100644 --- a/doc/internal-doc/api.sig +++ b/doc/internal-doc/api.sig @@ -1,5 +1,5 @@ #Signature file v4.1 -#Version 9.2.0-SNAPSHOT +#Version 9.2.0 CLSS public abstract com.xceptance.xlt.api.actions.AbstractAction cons protected init(com.xceptance.xlt.api.actions.AbstractAction,java.lang.String) @@ -1640,6 +1640,29 @@ meth public void unlock() supr java.lang.Object hfds configuration,lock +CLSS public com.xceptance.xlt.api.report.MovingAverageConfiguration +innr public final static !enum MovingAverageType +meth public com.xceptance.xlt.api.report.MovingAverageConfiguration$MovingAverageType getType() +meth public int getValue() +meth public java.lang.String getName() +meth public java.lang.String toString() +meth public static com.xceptance.xlt.api.report.MovingAverageConfiguration createPercentageConfig(int) +meth public static com.xceptance.xlt.api.report.MovingAverageConfiguration createTimeConfig(int) +meth public static com.xceptance.xlt.api.report.MovingAverageConfiguration createTimeConfig(int,java.lang.String) +supr java.lang.Object +hfds name,type,value + +CLSS public final static !enum com.xceptance.xlt.api.report.MovingAverageConfiguration$MovingAverageType + outer com.xceptance.xlt.api.report.MovingAverageConfiguration +fld public final static com.xceptance.xlt.api.report.MovingAverageConfiguration$MovingAverageType PERCENTAGE +fld public final static com.xceptance.xlt.api.report.MovingAverageConfiguration$MovingAverageType TIME +meth public java.lang.String getName() +meth public static com.xceptance.xlt.api.report.MovingAverageConfiguration$MovingAverageType valueOf(java.lang.String) +meth public static com.xceptance.xlt.api.report.MovingAverageConfiguration$MovingAverageType[] values() +meth public static java.util.List getNames() +supr java.lang.Enum +hfds name + CLSS public com.xceptance.xlt.api.report.PostProcessedDataContainer cons public init(int,int) fld public final int sampleFactor @@ -1666,12 +1689,13 @@ meth public boolean wantsDataRecords() CLSS public abstract interface com.xceptance.xlt.api.report.ReportProviderConfiguration meth public abstract boolean shouldChartsGenerated() +meth public abstract com.xceptance.xlt.api.report.MovingAverageConfiguration getCommonMovingAverageConfig() meth public abstract int getChartHeight() meth public abstract int getChartWidth() -meth public abstract int getMovingAveragePercentage() meth public abstract java.io.File getChartDirectory() meth public abstract java.io.File getCsvDirectory() meth public abstract java.io.File getReportDirectory() +meth public abstract java.util.List getAdditionalMovingAverageConfigs() meth public abstract java.util.Properties getProperties() meth public abstract long getChartEndTime() meth public abstract long getChartStartTime() @@ -2197,6 +2221,27 @@ intf java.lang.annotation.Annotation meth public abstract !hasdefault boolean forRemoval() meth public abstract !hasdefault java.lang.String since() +CLSS public abstract java.lang.Enum<%0 extends java.lang.Enum<{java.lang.Enum%0}>> +cons protected init(java.lang.String,int) +innr public final static EnumDesc +intf java.io.Serializable +intf java.lang.Comparable<{java.lang.Enum%0}> +intf java.lang.constant.Constable +meth protected final java.lang.Object clone() throws java.lang.CloneNotSupportedException +meth protected final void finalize() + anno 0 java.lang.Deprecated(boolean forRemoval=true, java.lang.String since="18") +meth public final boolean equals(java.lang.Object) +meth public final int compareTo({java.lang.Enum%0}) +meth public final int hashCode() +meth public final int ordinal() +meth public final java.lang.Class<{java.lang.Enum%0}> getDeclaringClass() +meth public final java.lang.String name() +meth public final java.util.Optional> describeConstable() +meth public java.lang.String toString() +meth public static <%0 extends java.lang.Enum<{%%0}>> {%%0} valueOf(java.lang.Class<{%%0}>,java.lang.String) +supr java.lang.Object +hfds hash,name,ordinal + CLSS public java.lang.Exception cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) cons public init() @@ -2299,6 +2344,9 @@ CLSS public abstract interface !annotation java.lang.annotation.Target intf java.lang.annotation.Annotation meth public abstract java.lang.annotation.ElementType[] value() +CLSS public abstract interface java.lang.constant.Constable +meth public abstract java.util.Optional describeConstable() + CLSS public abstract interface java.util.Collection<%0 extends java.lang.Object> intf java.lang.Iterable<{java.util.Collection%0}> meth public <%0 extends java.lang.Object> {%%0}[] toArray(java.util.function.IntFunction<{%%0}[]>) diff --git a/src/main/java/com/xceptance/common/util/AbstractConfiguration.java b/src/main/java/com/xceptance/common/util/AbstractConfiguration.java index 41881d618..31f141fcc 100644 --- a/src/main/java/com/xceptance/common/util/AbstractConfiguration.java +++ b/src/main/java/com/xceptance/common/util/AbstractConfiguration.java @@ -21,9 +21,12 @@ import java.net.URI; import java.net.URL; import java.text.ParseException; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Properties; import java.util.Set; +import java.util.TreeSet; import org.apache.commons.lang3.StringUtils; @@ -39,6 +42,10 @@ public class AbstractConfiguration */ private static final String PROPERTY_PARSING_ERROR_FORMAT = "The value '%s' of property '%s' cannot be resolved to a %s."; + static final String PROPERTY_PARSING_ERROR_INVALID_INDEX_FORMAT = "Index '%s' in properties with prefix '%s' is not a valid integer."; + + static final String PROPERTY_PARSING_ERROR_INDEX_OUT_OF_BOUNDS = "Index '%s' in properties with prefix '%s' is out of bounds. Index must be between '%d' and '%d'."; + /** * The internal store of properties. */ @@ -253,7 +260,7 @@ public long getLongProperty(final String key, final long defaultValue) * @return the property value * @throws RuntimeException * if the value cannot be found - * @see #getDoubleProperty(String, int) + * @see #getDoubleProperty(String, double) */ public double getDoubleProperty(final String key) { @@ -418,6 +425,72 @@ public Set getPropertyKeysWithPrefix(final String prefix) return set; } + /** + *

+ * Get a list of all integer indexes (in order) that are used in properties starting with the given prefix. + *

+ *

+ * For example, the properties contain: + *

+ * + *
{@code
+     * ...
+     * test.prop.1.type = typeA
+     * test.prop.1.value = valueA
+     * test.prop.3.type = typeB
+     * test.prop.3.value = valueB
+     * ...
+     * }
+     * 
+ *

+ * Calling this method with prefix "test.prop." will return a list containing [1,3]. + *

+ *

+ * This method will validate that all matching indexes are no smaller than "minIndex" and no bigger than "maxIndex", + * and throw an exception if any index is outside of this range. + *

+ * + * @param prefix + * the property prefix + * @param minIndex + * the smallest allowed index value + * @param maxIndex + * the biggest allowed index value + * @return a sorted list of all matching indexes + * @throws NumberFormatException + * if any of the indexes isn't a valid integer value + * @throws IndexOutOfBoundsException + * if any of the indexes is below the minIndex or above the maxIndex + */ + public List getPropertyKeyIndexes(final String prefix, final int minIndex, final int maxIndex) + { + final TreeSet sortedIndexes = new TreeSet<>(); + + for (final String indexString : getPropertyKeyFragment(prefix)) + { + final int index; + + try + { + index = ParseUtils.parseInt(indexString); + } + catch (final ParseException e) + { + throw new NumberFormatException(String.format(PROPERTY_PARSING_ERROR_INVALID_INDEX_FORMAT, indexString, prefix)); + } + + if (index < minIndex || index > maxIndex) + { + throw new IndexOutOfBoundsException(String.format(PROPERTY_PARSING_ERROR_INDEX_OUT_OF_BOUNDS, indexString, prefix, minIndex, + maxIndex)); + } + + sortedIndexes.add(index); + } + + return new ArrayList<>(sortedIndexes); + } + /** * Returns the value of the given property. If the property value cannot be found, an exception will be thrown. * @@ -512,8 +585,6 @@ public URL getUrlProperty(final String key, final URL defaultValue) * * @param key * the property name - * @param defaultValue - * the default property value * @return the property value * @throws RuntimeException * if there is no property configured for the argument key diff --git a/src/main/java/com/xceptance/common/util/ParseUtils.java b/src/main/java/com/xceptance/common/util/ParseUtils.java index aedad10b0..2c0e70312 100644 --- a/src/main/java/com/xceptance/common/util/ParseUtils.java +++ b/src/main/java/com/xceptance/common/util/ParseUtils.java @@ -299,6 +299,29 @@ public static long parseLong(final String s, final long defaultValue) } } + /** + * Parses a given integer percentage into an int value. The input can optionally end with a "%" character. E.g. the + * inputs "25%" or "25" will both return the value "25". + * + * @param s + * string to parse + * @return parsed percentage as an int value + * @throws ParseException + * if the input (ignoring the "%" character) can't be parsed as an int value + */ + public static int parseIntPercentage(final String s) throws ParseException + { + try + { + final String trimmedInput = s.trim(); + return parseInt(trimmedInput.endsWith("%") ? trimmedInput.substring(0, trimmedInput.length() - 1) : trimmedInput); + } + catch (ParseException e) + { + throw new ParseException(String.format("Failed to parse '%s' as int percentage value.", s), 0); + } + } + /** *

* Parses the given String as a relative (e.g. "-10" or "+10") or an absolute value (e.g. "10") and returns it. Use diff --git a/src/main/java/com/xceptance/xlt/api/report/MovingAverageConfiguration.java b/src/main/java/com/xceptance/xlt/api/report/MovingAverageConfiguration.java new file mode 100644 index 000000000..73353c618 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/api/report/MovingAverageConfiguration.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2005-2022 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.api.report; + +import java.util.Arrays; +import java.util.List; + +/** + * Holds the configuration for a moving average. + */ +public class MovingAverageConfiguration +{ + /** + * Moving average type. + */ + public enum MovingAverageType + { + /** + * Average over a percentage of data points. E.g. if we have 1000 data points, a percentage average of 5% will + * calculate the average over the last 50 data points. + */ + PERCENTAGE("percentage"), + + /** + * Average over a time interval. E.g. a time average of 30s will calculate the average over all data points in + * the last 30s. + */ + TIME("time"); + + /** + * The name used to reference the type, e.g. in property files. + */ + private final String name; + + MovingAverageType(final String name) + { + this.name = name; + } + + /** + * Get the name of the moving average type. + * + * @return the name of the moving average type + */ + public String getName() + { + return name; + } + + /** + * Get the names of all available moving average types. + * + * @return a list of all moving average type names + */ + public static List getNames() + { + return Arrays.stream(values()).map(MovingAverageType::getName).toList(); + } + } + + /** + * The type of the moving average configuration. + */ + private final MovingAverageType type; + + /** + * The value of the moving average configuration. For "percentage" averages this is an integer percentage value + * (e.g. a value of "25" means "25%"). For "time" averages this is a time interval in seconds. + */ + private final int value; + + /** + * The name of the moving average configuration. + */ + private final String name; + + private MovingAverageConfiguration(final MovingAverageType type, final int value, final String name) + { + this.type = type; + this.value = value; + this.name = name; + } + + /** + * Get the type of the moving average configuration. + * + * @return the type of the moving average configuration + */ + public MovingAverageType getType() + { + return type; + } + + /** + * Get the value of the moving average configuration. + * + * @return the value of the moving average configuration. For "percentage" averages this is a percentage integer + * value (e.g. a value of "25" means "25%"). For "time" averages this is a time interval in seconds. + */ + public int getValue() + { + return value; + } + + /** + * Get the name of the moving average configuration. + * + * @return the name of the moving average configuration + */ + public String getName() + { + return name; + } + + /** + * Create a moving average configuration of type "percentage" with the given percentage value. + * + * @param percentage + * an integer value representing the percentage (e.g. a value of "25" means "25%") + * @return the "percentage" moving average configuration + */ + public static MovingAverageConfiguration createPercentageConfig(final int percentage) + { + return new MovingAverageConfiguration(MovingAverageType.PERCENTAGE, percentage, percentage + "%"); + } + + /** + * Create a moving average configuration of type "time" with the given time interval in seconds. + * + * @param seconds + * an integer value representing a time interval in seconds (e.g. a value of "30" means that the average + * should be calculated over the last 30 seconds) + * @return the "time" moving average configuration + */ + public static MovingAverageConfiguration createTimeConfig(final int seconds) + { + return createTimeConfig(seconds, seconds + "s"); + } + + /** + * Create a moving average configuration of type "time" with the given time interval in seconds and the given custom + * name. This can be used to provide a name that is easier to comprehend than a number of seconds (e.g. the name + * "5m30s" might be more helpful than "330s"). + * + * @param seconds + * an integer value representing the time interval in seconds (e.g. a value of "30" means that the + * average should be calculated over the last 30 seconds) + * @param name + * the custom name for this configuration + * @return the "time" moving average configuration + */ + public static MovingAverageConfiguration createTimeConfig(final int seconds, final String name) + { + return new MovingAverageConfiguration(MovingAverageType.TIME, seconds, name); + } + + @Override + public String toString() + { + return String.format("MovingAverageConfiguration [type=%s, value=%d, name=%s]", type, value, name); + } +} diff --git a/src/main/java/com/xceptance/xlt/api/report/ReportProviderConfiguration.java b/src/main/java/com/xceptance/xlt/api/report/ReportProviderConfiguration.java index d90e3c131..9f4125e9b 100644 --- a/src/main/java/com/xceptance/xlt/api/report/ReportProviderConfiguration.java +++ b/src/main/java/com/xceptance/xlt/api/report/ReportProviderConfiguration.java @@ -16,6 +16,7 @@ package com.xceptance.xlt.api.report; import java.io.File; +import java.util.List; import java.util.Properties; /** @@ -81,12 +82,18 @@ public interface ReportProviderConfiguration public File getCsvDirectory(); /** - * Returns the preferred percentage of the available values used to calculate moving average values. For example, - * with 5 percent and 1000 values, the moving average is generated from the last 50 values. + * Returns the configuration for the common moving average (see {@link MovingAverageConfiguration}). * - * @return the percentage + * @return the common moving average configuration */ - public int getMovingAveragePercentage(); + public MovingAverageConfiguration getCommonMovingAverageConfig(); + + /** + * Returns a list of configurations for additional moving averages (see {@link MovingAverageConfiguration}). + * + * @return a list of additional moving average configurations + */ + public List getAdditionalMovingAverageConfigs(); /** * Returns all the settings from the file "xlt/config/reportgenerator.properties" as raw properties. Use these diff --git a/src/main/java/com/xceptance/xlt/common/XltConstants.java b/src/main/java/com/xceptance/xlt/common/XltConstants.java index 81c237e07..993435885 100644 --- a/src/main/java/com/xceptance/xlt/common/XltConstants.java +++ b/src/main/java/com/xceptance/xlt/common/XltConstants.java @@ -184,7 +184,7 @@ private XltConstants() * The prefix of custom log files. */ public static final String CUSTOM_LOG_PREFIX = "custom_log_"; - + /** * The option name of the from option on the command line. */ @@ -333,6 +333,11 @@ private XltConstants() */ public static final String REPORT_CHART_DIR = "charts"; + /** + * The maximum amount of additional moving averages that can be configured to appear in the "Averages" tabs. + */ + public static final int REPORT_CHART_MAX_ADDITIONAL_AVERAGES = 5; + /** * Placeholder file name for reports */ @@ -464,17 +469,19 @@ private XltConstants() /** * The name if we want to collect more request information */ - public static final String PROP_COLLECT_ADDITIONAL_REQUEST_DATA = XltConstants.XLT_PACKAGE_PATH + ".results.data.request.collectAdditionalRequestInfo"; + public static final String PROP_COLLECT_ADDITIONAL_REQUEST_DATA = XltConstants.XLT_PACKAGE_PATH + + ".results.data.request.collectAdditionalRequestInfo"; /** * The name if we want to collect the used IP address. */ - public static final String PROP_COLLECT_USED_IP_ADDRESS = XltConstants.XLT_PACKAGE_PATH + ".results.data.request.collectUsedIpAddress"; + public static final String PROP_COLLECT_USED_IP_ADDRESS = XltConstants.XLT_PACKAGE_PATH + ".results.data.request.collectUsedIpAddress"; /** * The name if we want to clean the user info */ - public static final String PROP_REMOVE_USERINFO_FROM_REQUEST_URL = XltConstants.XLT_PACKAGE_PATH + ".results.data.request.removeUserInfoFromURL"; + public static final String PROP_REMOVE_USERINFO_FROM_REQUEST_URL = XltConstants.XLT_PACKAGE_PATH + + ".results.data.request.removeUserInfoFromURL"; /** * The name of the property for specifying the test suite's data directory. diff --git a/src/main/java/com/xceptance/xlt/report/ReportGenerator.java b/src/main/java/com/xceptance/xlt/report/ReportGenerator.java index ce34d31f9..189b5e40d 100644 --- a/src/main/java/com/xceptance/xlt/report/ReportGenerator.java +++ b/src/main/java/com/xceptance/xlt/report/ReportGenerator.java @@ -45,8 +45,8 @@ import com.xceptance.xlt.mastercontroller.TestCaseLoadProfileConfiguration; import com.xceptance.xlt.mastercontroller.TestLoadProfileConfiguration; import com.xceptance.xlt.report.external.ExternalReportGenerator; -import com.xceptance.xlt.report.scorecard.Scorecard; import com.xceptance.xlt.report.scorecard.Evaluator; +import com.xceptance.xlt.report.scorecard.Scorecard; import com.xceptance.xlt.report.util.ConcurrentUsersTable; import com.xceptance.xlt.report.util.JFreeChartUtils; import com.xceptance.xlt.report.util.ReportUtils; diff --git a/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java b/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java index deb0ffff2..d734b2f41 100644 --- a/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java +++ b/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java @@ -19,12 +19,14 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.TreeSet; @@ -34,8 +36,11 @@ import org.apache.commons.vfs2.FileObject; import com.xceptance.common.util.AbstractConfiguration; +import com.xceptance.common.util.ParseUtils; import com.xceptance.common.util.RegExUtils; import com.xceptance.xlt.api.engine.Data; +import com.xceptance.xlt.api.report.MovingAverageConfiguration; +import com.xceptance.xlt.api.report.MovingAverageConfiguration.MovingAverageType; import com.xceptance.xlt.api.report.ReportProvider; import com.xceptance.xlt.api.report.ReportProviderConfiguration; import com.xceptance.xlt.api.util.XltException; @@ -162,14 +167,20 @@ public enum ChartCappingMode private static final String PROP_SUFFIX_ID = "id"; + static final String PROP_SUFFIX_TYPE = "type"; + + static final String PROP_SUFFIX_VALUE = "value"; + private static final String PROP_CHARTS_PREFIX = PROP_PREFIX + "charts."; + static final String PROP_CHARTS_AVERAGE_COMMON = PROP_CHARTS_PREFIX + "commonAverage."; + + static final String PROP_CHARTS_AVERAGES_ADDITIONAL = PROP_CHARTS_PREFIX + "averages."; + private static final String PROP_CHARTS_COMPRESSION_FACTOR = PROP_CHARTS_PREFIX + "compressionFactor"; private static final String PROP_CHARTS_HEIGHT = PROP_CHARTS_PREFIX + "height"; - private static final String PROP_CHARTS_MOV_AVG_PERCENTAGE = PROP_CHARTS_PREFIX + "movingAverage.percentageOfValues"; - private static final String PROP_CHARTS_WIDTH = PROP_CHARTS_PREFIX + "width"; private static final String PROP_DATA_RECORD_CLASSES_PREFIX = PROP_PREFIX + "dataRecords."; @@ -225,20 +236,36 @@ public enum ChartCappingMode private static final String PROP_DYNAMIC_CHARTS_ENABLED = PROP_PREFIX + "dynamicCharts.enabled"; + static final String ERROR_AVERAGE_INDEX_INVALID = "Invalid index in average configuration starting with '" + + PROP_CHARTS_AVERAGES_ADDITIONAL + "'."; + + static final String ERROR_AVERAGE_PERCENTAGE_OUT_OF_BOUNDS = "Percentage value configured in property '%s' must be between '1%%' and '100%%', but was '%s'."; + + static final String ERROR_AVERAGE_PROPERTY_MISSING = "Moving average configuration property '%s' exists, but required matching property '%s' isn't configured."; + + static final String ERROR_AVERAGE_TIME_OUT_OF_BOUNDS = "Time value configured in property '%s' must be greater than '0s', but was '%s'."; + + static final String ERROR_AVERAGE_TYPE_INVALID = "Moving average type '%s' configured in property '%s' is invalid. Valid types are: " + + MovingAverageType.getNames() + "."; + + static final String ERROR_INVALID_PROPERTY_VALUE_FORMAT = "Failed to read property '%s' because value format is invalid."; + private final float chartsCompressionFactor; private final int chartsHeight; private final int chartsWidth; + private final MovingAverageConfiguration commonMovingAverage; + + private final List additionalMovingAverages; + private final File configDirectory; private final Map> dataRecordClasses; private final File homeDirectory; - private final int movingAveragePoints; - private final List outputFileNames; private final List> reportProviderClasses; @@ -386,8 +413,31 @@ public ReportGeneratorConfiguration() throws IOException public ReportGeneratorConfiguration(Properties xltProperties, final File overridePropertyFile, final Properties commandLineProperties) throws IOException { - homeDirectory = XltExecutionContext.getCurrent().getXltHomeDir(); - configDirectory = XltExecutionContext.getCurrent().getXltConfigDir(); + this(XltExecutionContext.getCurrent().getXltHomeDir(), XltExecutionContext.getCurrent().getXltConfigDir(), xltProperties, + overridePropertyFile, commandLineProperties); + } + + /** + * Creates a new ReportGeneratorConfiguration object. Allows setting the XLT home and config directory manually. Use + * this only for unit tests or similar scenarios. + * + * @param home + * the XLT home directory + * @param config + * the XLT config directory + * @param overridePropertyFile + * Property file that overrides the basic one. This parameter might be null or empty + * @param commandLineProperties + * Properties set on command line. This parameter might be null. + * @throws IOException + * if an I/O error occurs + */ + ReportGeneratorConfiguration(final File home, final File config, Properties xltProperties, final File overridePropertyFile, + final Properties commandLineProperties) + throws IOException + { + homeDirectory = home; + configDirectory = config; if (xltProperties == null) { @@ -491,7 +541,8 @@ public ReportGeneratorConfiguration(Properties xltProperties, final File overrid chartsCompressionFactor = (float) getDoubleProperty(PROP_CHARTS_COMPRESSION_FACTOR, 0.0f); chartsWidth = getIntProperty(PROP_CHARTS_WIDTH, 900); chartsHeight = getIntProperty(PROP_CHARTS_HEIGHT, 300); - movingAveragePoints = getIntProperty(PROP_CHARTS_MOV_AVG_PERCENTAGE, 5); + commonMovingAverage = readCommonMovingAverageConfiguration(MovingAverageConfiguration.createPercentageConfig(5)); + additionalMovingAverages = readAdditionalMovingAverageConfigurations(new ArrayList<>()); dynamicChartsEnabled = getBooleanProperty(PROP_DYNAMIC_CHARTS_ENABLED, true); @@ -767,9 +818,18 @@ public File getHomeDirectory() * {@inheritDoc} */ @Override - public int getMovingAveragePercentage() + public MovingAverageConfiguration getCommonMovingAverageConfig() + { + return commonMovingAverage; + } + + /** + * {@inheritDoc} + */ + @Override + public List getAdditionalMovingAverageConfigs() { - return movingAveragePoints; + return additionalMovingAverages; } public List getOutputFileNames() @@ -1714,4 +1774,127 @@ private Pattern compileRegEx(final String regEx, final String propertyName) propertyName, ex.getMessage())); } } + + /** + * Read the configuration for the common average from the properties. If no settings for the common average are + * provided, return the given default value instead. + * + * @param defaultValue + * the default configuration to return if the properties contain no settings for the common average + * @return the common moving average configuration from the properties or the default value + */ + private MovingAverageConfiguration readCommonMovingAverageConfiguration(final MovingAverageConfiguration defaultValue) + { + return Optional.ofNullable(readMovingAverageConfiguration(PROP_CHARTS_AVERAGE_COMMON)).orElse(defaultValue); + } + + /** + * Read the configurations for the additional moving averages from the properties. If no additional moving averages + * are configured, return the given default values instead. + * + * @param defaultValues + * the default configurations to return if the properties contain no settings for additional moving + * averages + * @return the additional moving average configurations from the properties or the default value + */ + private List readAdditionalMovingAverageConfigurations(final List defaultValues) + { + // Get the numeric indexes of all additional averages in the properties and verify they are within the expected + // limits + final List indexes; + try + { + indexes = getPropertyKeyIndexes(PROP_CHARTS_AVERAGES_ADDITIONAL, 1, XltConstants.REPORT_CHART_MAX_ADDITIONAL_AVERAGES); + } + catch (final NumberFormatException | IndexOutOfBoundsException e) + { + throw new XltException(ERROR_AVERAGE_INDEX_INVALID, e); + } + + // Read all additional moving average configurations + final List additionalAverages = new ArrayList<>(); + for (final int index : indexes) + { + final MovingAverageConfiguration avg = readMovingAverageConfiguration(PROP_CHARTS_AVERAGES_ADDITIONAL + index + "."); + if (avg != null) + { + additionalAverages.add(avg); + } + } + + // If no additional averages were configured in the properties, return the default values + return additionalAverages.isEmpty() ? defaultValues : additionalAverages; + } + + /** + * Read the moving average configuration (type and value) from the properties starting with the given prefix. + * + * @param prefix + * the prefix of the configuration properties + * @return the resulting moving average configuration, or "null" if there is no configuration with the given prefix + * @throws XltException + * if the moving average configuration is incomplete (either type or value are missing), the type is + * invalid, the value format is invalid, or the value is outside the allowed ranges + */ + private MovingAverageConfiguration readMovingAverageConfiguration(final String prefix) + { + final String typeKey = prefix + PROP_SUFFIX_TYPE; + final String valueKey = prefix + PROP_SUFFIX_VALUE; + + final String typeString = getStringProperty(typeKey, null); + final String valueString = getStringProperty(valueKey, null); + + // If the average isn't configured in the properties, return "null" + if (StringUtils.isBlank(typeString) && StringUtils.isBlank(valueString)) + { + return null; + } + + // Fail if value is configured, but type is missing + if (StringUtils.isBlank(typeString)) + { + throw new XltException(String.format(ERROR_AVERAGE_PROPERTY_MISSING, valueKey, typeKey)); + } + + // Fail if type is configured, but value is missing + if (StringUtils.isBlank(valueString)) + { + throw new XltException(String.format(ERROR_AVERAGE_PROPERTY_MISSING, typeKey, valueKey)); + } + + try + { + // Handle "percentage" type average + if (MovingAverageType.PERCENTAGE.getName().equals(typeString)) + { + final int percentage = ParseUtils.parseIntPercentage(valueString); + if (percentage <= 0 || percentage > 100) + { + throw new XltException(String.format(ERROR_AVERAGE_PERCENTAGE_OUT_OF_BOUNDS, valueKey, valueString)); + } + return MovingAverageConfiguration.createPercentageConfig(percentage); + } + + // Handle "time" type average + if (MovingAverageType.TIME.getName().equals(typeString)) + { + final int seconds = ParseUtils.parseTimePeriod(valueString); + if (seconds <= 0) + { + throw new XltException(String.format(ERROR_AVERAGE_TIME_OUT_OF_BOUNDS, valueKey, valueString)); + } + // If the input was only a numeric value, append an "s" to the name to signal that it's a "second" count + final String name = valueString.equals(String.valueOf(seconds)) ? (valueString + "s") : valueString; + return MovingAverageConfiguration.createTimeConfig(seconds, name); + } + + // Fail if type isn't recognized + throw new XltException(String.format(ERROR_AVERAGE_TYPE_INVALID, typeString, typeKey)); + } + catch (ParseException e) + { + // Throw an exception if we failed to parse the percentage or time value + throw new XltException(String.format(ERROR_INVALID_PROPERTY_VALUE_FORMAT, valueKey), e); + } + } } diff --git a/src/main/java/com/xceptance/xlt/report/external/converter/CustomReportProvider.java b/src/main/java/com/xceptance/xlt/report/external/converter/CustomReportProvider.java index 27e4c9578..87e71e366 100644 --- a/src/main/java/com/xceptance/xlt/report/external/converter/CustomReportProvider.java +++ b/src/main/java/com/xceptance/xlt/report/external/converter/CustomReportProvider.java @@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils; import org.jfree.data.time.TimeSeries; +import com.xceptance.xlt.api.report.MovingAverageConfiguration; import com.xceptance.xlt.api.util.XltLogger; import com.xceptance.xlt.report.external.config.ChartConfig; import com.xceptance.xlt.report.external.config.SeriesConfig; @@ -441,8 +442,8 @@ private void createChart(final ChartConfig config, final String fileName) { try { - final int percentage = Integer.parseInt(seriesConfig.getAverage()); - final TimeSeries avgTimeSeries = JFreeChartUtils.createMovingAverageTimeSeries(timeSeries, percentage); + final MovingAverageConfiguration averageConfig = MovingAverageConfiguration.createPercentageConfig(Integer.parseInt(seriesConfig.getAverage())); + final TimeSeries avgTimeSeries = JFreeChartUtils.createMovingAverageTimeSeries(timeSeries, averageConfig); final Color avgColor = getColor(seriesConfig.getAverageColor()); final TimeSeriesConfiguration avgTsConfig = new TimeSeriesConfiguration(avgTimeSeries, avgColor, Style.LINE); axisCollections.get(seriesConfig.getAxis() - 1).add(avgTsConfig); @@ -450,7 +451,7 @@ private void createChart(final ChartConfig config, final String fileName) catch (final NumberFormatException e) { XltLogger.reportLogger.warn("Skip moving average for series " + valueName + ". Can not parse average '" + - seriesConfig.getAverage() + "' to integer value"); + seriesConfig.getAverage() + "' to integer value"); } } diff --git a/src/main/java/com/xceptance/xlt/report/providers/AbstractDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/AbstractDataProcessor.java index 08392282e..deb4aac19 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/AbstractDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/AbstractDataProcessor.java @@ -16,9 +16,11 @@ package com.xceptance.xlt.report.providers; import java.io.File; +import java.util.List; import com.xceptance.xlt.api.engine.Data; import com.xceptance.xlt.api.report.AbstractReportProvider; +import com.xceptance.xlt.api.report.MovingAverageConfiguration; import com.xceptance.xlt.api.report.ReportProviderConfiguration; import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartCappingInfo; @@ -36,7 +38,9 @@ public abstract class AbstractDataProcessor private File csvDir; - private int movingAveragePercentage; + private MovingAverageConfiguration commonMovingAverageConfig; + + private List additionalMovingAverageConfigs; private ChartCappingInfo chartCappingInfo; @@ -71,7 +75,8 @@ protected AbstractDataProcessor(final String name, final AbstractReportProvider setChartWidth(config.getChartWidth()); setChartHeight(config.getChartHeight()); - setMovingAveragePercentage(config.getMovingAveragePercentage()); + setCommonMovingAverageConfig(config.getCommonMovingAverageConfig()); + setAdditionalMovingAverageConfigs(config.getAdditionalMovingAverageConfigs()); } /** @@ -136,13 +141,23 @@ public long getEndTime() } /** - * Returns the value of the 'movingAveragePercentage' attribute. + * Returns the common moving average configuration. + * + * @return the common moving average configuration + */ + public MovingAverageConfiguration getCommonMovingAverageConfig() + { + return commonMovingAverageConfig; + } + + /** + * Returns the list of all additional moving average configurations. * - * @return the value of movingAveragePercentage + * @return the additional moving average configurations */ - public int getMovingAveragePercentage() + public List getAdditionalMovingAverageConfigs() { - return movingAveragePercentage; + return additionalMovingAverageConfigs; } /** @@ -243,14 +258,25 @@ public void setCsvDir(final File csvDir) } /** - * Sets the new value of the 'movingAveragePercentage' attribute. + * Sets the new value of the common moving average configuration. + * + * @param commonMovingAverageConfig + * the new common moving average configuration + */ + public void setCommonMovingAverageConfig(final MovingAverageConfiguration commonMovingAverageConfig) + { + this.commonMovingAverageConfig = commonMovingAverageConfig; + } + + /** + * Sets the additional moving average configurations. * - * @param movingAveragePercentage - * the new movingAveragePercentage value + * @param additionalMovingAverageConfigs + * the new list of all additional moving average configurations */ - public void setMovingAveragePercentage(final int movingAveragePercentage) + public void setAdditionalMovingAverageConfigs(final List additionalMovingAverageConfigs) { - this.movingAveragePercentage = movingAveragePercentage; + this.additionalMovingAverageConfigs = additionalMovingAverageConfigs; } /** diff --git a/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java index 3655016df..cc1c4193f 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java @@ -28,9 +28,9 @@ import com.xceptance.xlt.report.util.ArithmeticMean; import com.xceptance.xlt.report.util.DoubleMinMaxValueSet; import com.xceptance.xlt.report.util.DoubleSummaryStatistics; +import com.xceptance.xlt.report.util.IntMinMaxValueSet; import com.xceptance.xlt.report.util.JFreeChartUtils; import com.xceptance.xlt.report.util.MinMaxTimeSeriesCollection; -import com.xceptance.xlt.report.util.IntMinMaxValueSet; import com.xceptance.xlt.report.util.ReportUtils; import com.xceptance.xlt.report.util.TaskManager; @@ -158,7 +158,7 @@ public AgentReport createAgentReport() agentReport.transactions = transactions; agentReport.transactionErrors = transactionErrors; - + agentReport.transactionErrorPercentage = ReportUtils.calculatePercentage(transactionErrors, transactions); return agentReport; @@ -260,7 +260,7 @@ protected void createCpuUsageChart(final String agentName, final File outputDir) final TimeSeriesCollection cpuTimeSeriesCollection = new TimeSeriesCollection(); final TimeSeries cpuTimeSeries = JFreeChartUtils.toMinMaxTimeSeries(cpuUsageValueSet, "Agent CPU Usage"); - final TimeSeries avgCpuTimeSeries = JFreeChartUtils.createMovingAverageTimeSeries(cpuTimeSeries, getMovingAveragePercentage()); + final TimeSeries avgCpuTimeSeries = JFreeChartUtils.createMovingAverageTimeSeries(cpuTimeSeries, getCommonMovingAverageConfig()); final TimeSeries gcCpuTimeSeries = JFreeChartUtils.toMinMaxTimeSeries(gcCpuUsageValueSet, "Agent GC CPU Usage"); final TimeSeries totalCpuUsageTimeSeries = JFreeChartUtils.toMinMaxTimeSeries(totalCpuUsageValueSet, "Total CPU Usage"); @@ -296,7 +296,7 @@ protected void createMemoryUsageChart(final String name, final File outputDir) final TimeSeries totalHeapSeries = JFreeChartUtils.toMinMaxTimeSeries(totalHeapValueSet, "Total Heap"); // final TimeSeries usedMemSeries = JFreeChartUtils.toMinMaxTimeSeries(usedMemValueSet, "Used Physical Memory"); - final TimeSeries usedHeapAvgSeries = JFreeChartUtils.createMovingAverageTimeSeries(usedHeapSeries, getMovingAveragePercentage()); + final TimeSeries usedHeapAvgSeries = JFreeChartUtils.createMovingAverageTimeSeries(usedHeapSeries, getCommonMovingAverageConfig()); memoryCollection.addSeries(usedHeapAvgSeries); memoryCollection.addSeries(usedHeapSeries); diff --git a/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java index 2bc4f04d5..a01116b96 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.List; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.CombinedDomainXYPlot; @@ -144,8 +145,15 @@ public TimerReport createTimerReport(final boolean generateHistogram) { // post-process the run time series now as they will be needed for multiple charts final TimeSeries runTimeTimeSeries = JFreeChartUtils.toMinMaxTimeSeries(runTimeValueSet, "Runtime"); + + // process common moving average final TimeSeries runTimeAverageTimeSeries = JFreeChartUtils.createMovingAverageTimeSeries(runTimeTimeSeries, - getMovingAveragePercentage()); + getCommonMovingAverageConfig()); + // process additional moving averages, if they are configured + final List additionalRunTimeAverageTimeSeriesList = getAdditionalMovingAverageConfigs().stream() + .map(config -> JFreeChartUtils.createMovingAverageTimeSeries(runTimeTimeSeries, + config)) + .toList(); // create charts asynchronously final TaskManager taskManager = TaskManager.getInstance(); @@ -175,7 +183,7 @@ public void run() public void run() { saveResponseTimeAverageChart(name, runTimeTimeSeries, runTimeAverageTimeSeries, timerReport.median.doubleValue(), - timerReport.mean.doubleValue()); + timerReport.mean.doubleValue(), additionalRunTimeAverageTimeSeriesList); } }); @@ -343,12 +351,13 @@ protected JFreeChart createResponseTimeAndErrorsChart(final String chartTitle, f * the mean of the values in the response times series */ private JFreeChart createResponseTimeAverageChart(final String name, final TimeSeries responseTimeSeries, - final TimeSeries responseTimeAverageSeries, final double median, final double mean) + final TimeSeries responseTimeAverageSeries, final double median, final double mean, + final List additionalResponseTimeAverageSeriesList) { // create and setup chart final JFreeChart chart = JFreeChartUtils.createAverageLineChart("Runtime", name, "Runtime [ms]", responseTimeSeries, responseTimeAverageSeries, median, mean, getStartTime(), - getEndTime()); + getEndTime(), additionalResponseTimeAverageSeriesList); return chart; } @@ -379,7 +388,7 @@ private void saveCountPerSecondChart(final String timerName, final TimeSeries ti // final long start = TimerUtils.getTime(); final JFreeChart chart = JFreeChartUtils.createLineChart(timerName, "Count", timeSeries, getStartTime(), getEndTime(), true, - getMovingAveragePercentage()); + getCommonMovingAverageConfig()); JFreeChartUtils.saveChart(chart, timerName + "_CountPerSecond", getChartDir(), getChartWidth(), getChartHeight()); @@ -402,13 +411,15 @@ private void saveCountPerSecondChart(final String timerName, final TimeSeries ti * the mean of the values in the response times series */ private void saveResponseTimeAverageChart(final String timerName, final TimeSeries responseTimeSeries, - final TimeSeries responseTimeAverageSeries, final double median, final double mean) + final TimeSeries responseTimeAverageSeries, final double median, final double mean, + final List additionalResponseTimeAverageSeriesList) { // System.out.println("Creating average chart for timer '" + timerName + "' ... "); // final long start = TimerUtils.getTime(); - final JFreeChart chart = createResponseTimeAverageChart(timerName, responseTimeSeries, responseTimeAverageSeries, median, mean); + final JFreeChart chart = createResponseTimeAverageChart(timerName, responseTimeSeries, responseTimeAverageSeries, median, mean, + additionalResponseTimeAverageSeriesList); JFreeChartUtils.saveChart(chart, timerName + "_Average", getChartDir(), getChartWidth(), getChartHeight()); @@ -462,7 +473,7 @@ private void saveResponseTimeChart(final String timerName, final TimeSeries resp /** * Writes the response time series data to a JSON file in the charts directory. The data in this file is later read * from the load test report and forms the basis for interactive charts. - * + * * @param timerName * the name of the timer * @param responseTimeSeries diff --git a/src/main/java/com/xceptance/xlt/report/providers/ConfigurationReportProvider.java b/src/main/java/com/xceptance/xlt/report/providers/ConfigurationReportProvider.java index eff0bd9b0..648f01846 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/ConfigurationReportProvider.java +++ b/src/main/java/com/xceptance/xlt/report/providers/ConfigurationReportProvider.java @@ -27,7 +27,6 @@ import java.util.Properties; import java.util.TreeMap; -import com.xceptance.xlt.report.ReportGeneratorConfiguration; import org.apache.commons.lang3.StringUtils; import org.apache.commons.vfs2.FileSystemException; import org.apache.commons.vfs2.FileSystemManager; @@ -40,6 +39,7 @@ import com.xceptance.xlt.common.XltConstants; import com.xceptance.xlt.mastercontroller.TestCaseLoadProfileConfiguration; import com.xceptance.xlt.mastercontroller.TestLoadProfileConfiguration; +import com.xceptance.xlt.report.ReportGeneratorConfiguration; import com.xceptance.xlt.report.util.ReportUtils; import com.xceptance.xlt.util.PropertiesConfigurationException; import com.xceptance.xlt.util.PropertiesIOException; diff --git a/src/main/java/com/xceptance/xlt/report/providers/CustomValueProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/CustomValueProcessor.java index c95baca7d..7e009070c 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/CustomValueProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/CustomValueProcessor.java @@ -182,7 +182,8 @@ private CustomValueReport createReportFragment(final String samplerName, final S { // create the value series now as they will be needed for multiple charts final TimeSeries valueSeries = JFreeChartUtils.toMinMaxTimeSeries(vSet, samplerName); - final TimeSeries averageValueSeries = JFreeChartUtils.createMovingAverageTimeSeries(valueSeries, getMovingAveragePercentage()); + final TimeSeries averageValueSeries = JFreeChartUtils.createMovingAverageTimeSeries(valueSeries, + getCommonMovingAverageConfig()); final XYIntervalSeries histogramSeries = histogram.toSeries("Counts"); // create charts asynchronously @@ -252,8 +253,10 @@ public void run() * the title of the range axis * @param chartFileName * the name of the chart file - * @param valueSeries - * the value series + * @param timeSeries + * the time series + * @param averageTimeSeries + * the average time series * @param histogramSeries * the histogram series * @param chartCappingValue @@ -269,8 +272,8 @@ private void createCustomValueChart(final String samplerName, final String chart // create the value plot final DateAxis timeAxis = JFreeChartUtils.createTimeAxis(getStartTime(), getEndTime()); - final XYPlot customValuePlot = JFreeChartUtils.createLinePlot(timeSeries, averageTimeSeries, timeAxis, yAxisTitle, true, - chartScale, chartCappingValue); + final XYPlot customValuePlot = JFreeChartUtils.createLinePlot(timeSeries, averageTimeSeries, timeAxis, yAxisTitle, true, chartScale, + chartCappingValue); NumberAxis rangeAxis = (NumberAxis) customValuePlot.getRangeAxis(); rangeAxis.setStandardTickUnits(NumberAxis.createStandardTickUnits()); // rangeAxis.setAutoRangeIncludesZero(false); @@ -320,7 +323,7 @@ private void createCustomValueAverageChart(final String name, final String chart // create and customize the chart final JFreeChart chart = JFreeChartUtils.createAverageLineChart(name, chartTitle, yAxisTitle, valueSeries, averageValueSeries, - median, mean, getStartTime(), getEndTime()); + median, mean, getStartTime(), getEndTime(), null); chart.getXYPlot().getRangeAxis().setStandardTickUnits(NumberAxis.createStandardTickUnits()); // finally save the chart diff --git a/src/main/java/com/xceptance/xlt/report/providers/GeneralReportProvider.java b/src/main/java/com/xceptance/xlt/report/providers/GeneralReportProvider.java index 1e7db0aa5..1661dd716 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/GeneralReportProvider.java +++ b/src/main/java/com/xceptance/xlt/report/providers/GeneralReportProvider.java @@ -32,9 +32,9 @@ import com.xceptance.xlt.api.report.AbstractReportProvider; import com.xceptance.xlt.api.report.ReportProviderConfiguration; import com.xceptance.xlt.report.util.ConcurrentUsersTable; +import com.xceptance.xlt.report.util.IntMinMaxValueSet; import com.xceptance.xlt.report.util.JFreeChartUtils; import com.xceptance.xlt.report.util.JFreeChartUtils.ColorSet; -import com.xceptance.xlt.report.util.IntMinMaxValueSet; import com.xceptance.xlt.report.util.TaskManager; import com.xceptance.xlt.report.util.ValueSet; @@ -154,7 +154,7 @@ public void run() totalTransactionsValueSet, minMaxValueSetSize, "Error Rate"); final TimeSeries errorRateAverageTimeSeries = JFreeChartUtils.createMovingAverageTimeSeries(errorRateTimeSeries, - getConfiguration().getMovingAveragePercentage()); + getConfiguration().getCommonMovingAverageConfig()); createErrorsChart(failedTransactionsTimeSeries, errorRateAverageTimeSeries, "Transaction Errors", "TransactionErrors", chartsDir); @@ -247,7 +247,7 @@ protected void createChart(final TimeSeries timeSeries, final boolean showMoving final JFreeChart chart = JFreeChartUtils.createLineChart(title, yAxisTitle, timeSeries, config.getChartStartTime(), config.getChartEndTime(), showMovingAverage, - config.getMovingAveragePercentage()); + config.getCommonMovingAverageConfig()); JFreeChartUtils.saveChart(chart, fileName, outputDir, config.getChartWidth(), config.getChartHeight()); diff --git a/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java index 6305a84a3..ab27ef1f6 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java @@ -43,10 +43,10 @@ import com.xceptance.xlt.report.ReportGeneratorConfiguration; import com.xceptance.xlt.report.util.HistogramValueSet; import com.xceptance.xlt.report.util.IntMinMaxValueSet; +import com.xceptance.xlt.report.util.IntSummaryStatistics; import com.xceptance.xlt.report.util.JFreeChartUtils; import com.xceptance.xlt.report.util.ReportUtils; import com.xceptance.xlt.report.util.SegmentationValueSet; -import com.xceptance.xlt.report.util.IntSummaryStatistics; import com.xceptance.xlt.report.util.TaskManager; import net.agkn.hll.HLL; @@ -265,12 +265,14 @@ public void run() // just int is safe, more than 2 billion urls is unlikely timerReport.urls = getUrlList(distinctUrlSet, (int) distinctUrlsHLL.cardinality()); timerReport.countPerInterval = countPerSegment != null ? countPerSegment.getCountPerSegment() : ArrayUtils.EMPTY_INT_ARRAY; - timerReport.percentagePerInterval = countPerSegment != null ? new BigDecimal[countPerSegment.getCountPerSegment().length] : new BigDecimal[]{}; + timerReport.percentagePerInterval = countPerSegment != null ? new BigDecimal[countPerSegment.getCountPerSegment().length] + : new BigDecimal[] {}; if (countPerSegment != null) { for (int n = 0; n < countPerSegment.getCountPerSegment().length; n++) { - timerReport.percentagePerInterval[n] = ReportUtils.calculatePercentage(countPerSegment.getCountPerSegment()[n], timerReport.count); + timerReport.percentagePerInterval[n] = ReportUtils.calculatePercentage(countPerSegment.getCountPerSegment()[n], + timerReport.count); } } @@ -358,7 +360,7 @@ protected void createResponseSizeChart(final String timerName, final TimeSeries // final long start = TimerUtils.getTime(); final JFreeChart chart = JFreeChartUtils.createLineChart(timerName, "Bytes", timeSeries, getStartTime(), getEndTime(), true, - getMovingAveragePercentage()); + getCommonMovingAverageConfig()); JFreeChartUtils.saveChart(chart, timerName + "_ResponseSize", getChartDir(), getChartWidth(), getChartHeight()); // System.out.printf("OK (%,d values, %,d ms)\n", timeSeries.getItemCount(), TimerUtils.getTime() - start); diff --git a/src/main/java/com/xceptance/xlt/report/providers/TransactionDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/TransactionDataProcessor.java index 4dde7610f..aeaa8d12b 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/TransactionDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/TransactionDataProcessor.java @@ -122,7 +122,7 @@ public void run() "Current Arrival Rate"); final TimeSeries averagedArrivalRateTS = JFreeChartUtils.createMovingAverageTimeSeries(arrivalRateTS, - getMovingAveragePercentage()); + getCommonMovingAverageConfig()); averagedArrivalRateTS.setKey("Current Arrival Rate"); createChart(averagedArrivalRateTS, true, getName(), "Arrival Rate [1/h]", getName() + "_ArrivalRate", getChartDir(), @@ -168,8 +168,8 @@ public void processDataRecord(final Data data) @Override protected JFreeChart createResponseTimeAndErrorsChart(final String name, final TimeSeries responseTimeSeries, final TimeSeries responseTimeAverageSeries, - final XYIntervalSeries responseTimeHistogramSeries, - final TimeSeries errorsSeries, final int chartCappingValue) + final XYIntervalSeries responseTimeHistogramSeries, final TimeSeries errorsSeries, + final int chartCappingValue) { final JFreeChart chart = super.createResponseTimeAndErrorsChart(name, responseTimeSeries, responseTimeAverageSeries, responseTimeHistogramSeries, errorsSeries, chartCappingValue); @@ -191,7 +191,7 @@ protected JFreeChart createResponseTimeAndErrorsChart(final String name, final T getCountPerSecondValueSet(), minMaxValueSetSize, "Error Rate"); final TimeSeries errorRateAverageTimeSeries = JFreeChartUtils.createMovingAverageTimeSeries(errorRateTimeSeries, - getMovingAveragePercentage()); + getCommonMovingAverageConfig()); // create the error rate plot final XYPlot errorRatePlot = JFreeChartUtils.createLinePlot(new TimeSeriesCollection(errorRateAverageTimeSeries), null, @@ -256,13 +256,13 @@ protected void createChart(final TimeSeries timeSeries, final boolean showMoving { final ReportProviderConfiguration config = getConfiguration(); - //System.out.printf("Creating %s chart for timer '%s' ...\n", chartType, title); + // System.out.printf("Creating %s chart for timer '%s' ...\n", chartType, title); // final long start = TimerUtils.getTime(); final JFreeChart chart = JFreeChartUtils.createLineChart(title, yAxisTitle, timeSeries, config.getChartStartTime(), config.getChartEndTime(), showMovingAverage, - config.getMovingAveragePercentage(), showDots); + config.getCommonMovingAverageConfig(), showDots); JFreeChartUtils.saveChart(chart, fileName, outputDir, config.getChartWidth(), config.getChartHeight()); diff --git a/src/main/java/com/xceptance/xlt/report/util/JFreeChartUtils.java b/src/main/java/com/xceptance/xlt/report/util/JFreeChartUtils.java index 791809824..58dce145a 100644 --- a/src/main/java/com/xceptance/xlt/report/util/JFreeChartUtils.java +++ b/src/main/java/com/xceptance/xlt/report/util/JFreeChartUtils.java @@ -61,7 +61,7 @@ import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.Range; -import org.jfree.data.time.MovingAverage; +import org.jfree.data.time.RegularTimePeriod; import org.jfree.data.time.Second; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; @@ -75,6 +75,7 @@ import com.luciad.imageio.webp.WebPWriteParam; import com.xceptance.common.io.FileUtils; +import com.xceptance.xlt.api.report.MovingAverageConfiguration; import com.xceptance.xlt.common.XltConstants; import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartCappingInfo; import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartCappingInfo.ChartCappingMode; @@ -93,7 +94,10 @@ public static class ColorSet /** * blue, ... */ - public static final ColorSet AVERAGES = new ColorSet(COLOR_MOVING_AVERAGE, COLOR_MEDIAN, COLOR_MEAN); + public static final ColorSet AVERAGES = new ColorSet(COLOR_MOVING_AVERAGE, COLOR_MEDIAN, COLOR_MEAN, + COLOR_MOVING_AVERAGE_ADDITIONAL_1, COLOR_MOVING_AVERAGE_ADDITIONAL_2, + COLOR_MOVING_AVERAGE_ADDITIONAL_3, COLOR_MOVING_AVERAGE_ADDITIONAL_4, + COLOR_MOVING_AVERAGE_ADDITIONAL_5); /** * blue, gray, magenta, green, red @@ -164,14 +168,14 @@ public int size() */ public enum MoreColors { - BROWN(0xB97A57), - GRAY(0xAAAAAA), - GREEN(0x00AA00), - LIGHT_GRAY(0x757575), - LIGHT_GREEN(0xB5E61D), - LILAC(0xC8BFE7), - ORANGE(0xFF9900), - STEEL_BLUE(0x7092BE); + BROWN(0xB97A57), + GRAY(0xAAAAAA), + GREEN(0x00AA00), + LIGHT_GRAY(0x757575), + LIGHT_GREEN(0xB5E61D), + LILAC(0xC8BFE7), + ORANGE(0xFF9900), + STEEL_BLUE(0x7092BE); private final Color color; @@ -222,15 +226,40 @@ public Color getColor() public static final Color COLOR_MEAN = new Color(0xCD3333); /** - * The color of a median line in the charts (dark turquoise). + * The color of a median line in the charts (dark gray). */ - public static final Color COLOR_MEDIAN = new Color(0x62C0E0); + public static final Color COLOR_MEDIAN = Color.DARK_GRAY; /** * The color of a moving average line in the charts (dark blue). */ public static final Color COLOR_MOVING_AVERAGE = new Color(0x1C1CBF); + /** + * The color of an additional moving average line in the charts (light green). + */ + public static final Color COLOR_MOVING_AVERAGE_ADDITIONAL_1 = new Color(0xB2DF8B); + + /** + * The color of an additional moving average line in the charts (light orange). + */ + public static final Color COLOR_MOVING_AVERAGE_ADDITIONAL_2 = new Color(0xFDBF6D); + + /** + * The color of an additional moving average line in the charts (light purple). + */ + public static final Color COLOR_MOVING_AVERAGE_ADDITIONAL_3 = new Color(0xCAB2D7); + + /** + * The color of an additional moving average line in the charts (light blue). + */ + public static final Color COLOR_MOVING_AVERAGE_ADDITIONAL_4 = new Color(0xA6CDE3); + + /** + * The color of an additional moving average line in the charts (light pink). + */ + public static final Color COLOR_MOVING_AVERAGE_ADDITIONAL_5 = new Color(0xF6A9A9); + /** * The default chart theme. */ @@ -348,7 +377,8 @@ public static void capPlot(final XYPlot plot, final int cappingValue, final bool } /** - * Creates an average chart with the moving average, the median, and the mean, but not the actual values. + * Creates an average chart with the moving average, the median, the mean, and (optional) additional moving + * averages, but not the actual values. * * @param seriesName * the name of the series @@ -368,13 +398,16 @@ public static void capPlot(final XYPlot plot, final int cappingValue, final bool * chart start time * @param endTime * chart end time + * @param additionalAverageValueSeriesList + * list of additional average value series to add to the chart */ public static JFreeChart createAverageLineChart(final String seriesName, final String chartTitle, final String yAxisTitle, final TimeSeries valueSeries, final TimeSeries averageValueSeries, final double median, - final double mean, final long startTime, final long endTime) + final double mean, final long startTime, final long endTime, + final List additionalAverageValueSeriesList) { - final TimeSeries medianSeries = new TimeSeries(seriesName + " (Median)"); - final TimeSeries meanSeries = new TimeSeries(seriesName + " (Mean)"); + final TimeSeries medianSeries = new TimeSeries(seriesName + " Median"); + final TimeSeries meanSeries = new TimeSeries(seriesName + " Mean"); final TimeSeriesCollection seriesCollection = new TimeSeriesCollection(); seriesCollection.addSeries(averageValueSeries); @@ -395,6 +428,18 @@ public static JFreeChart createAverageLineChart(final String seriesName, final S meanSeries.add(lastItem.getPeriod(), mean); } + // add additional averages if there are any + if (additionalAverageValueSeriesList != null) + { + // don't add more than the allowed maximum of additional averages + final int maxAdditionalAveragesCount = Math.min(additionalAverageValueSeriesList.size(), + XltConstants.REPORT_CHART_MAX_ADDITIONAL_AVERAGES); + for (int i = 0; i < maxAdditionalAveragesCount; i++) + { + seriesCollection.addSeries(additionalAverageValueSeriesList.get(i)); + } + } + // create and customize the chart final JFreeChart chart = createLineChart(chartTitle, yAxisTitle, seriesCollection, startTime, endTime, ColorSet.AVERAGES); @@ -596,7 +641,7 @@ public static CombinedDomainXYPlot createCombinedPlot(final long startTime, fina * @param endTime * chart end time * @return the chart - * @see {@link JFreeChartUtils#addLinePlotToCombinedPlotChart(JFreeChart, String, TimeSeriesCollection)} + * @see {@link JFreeChartUtils#addLinePlotToCombinedPlotChart(JFreeChart, String, XYDataset)} */ public static JFreeChart createCombinedPlotChart(final String chartTitle, final long startTime, final long endTime) { @@ -666,15 +711,15 @@ public static XYPlot createHistogramPlot(final XYIntervalSeries histogramSeries, * the end time of the x-axis * @param includeMovingAverage * whether or not an additional moving average time series should be included - * @param percentage - * the percentaged amount of values for building the moving average + * @param movingAverageConfig + * the configuration for building the moving average * @return the chart */ public static JFreeChart createLineChart(final String chartTitle, final String rangeAxisTitle, final TimeSeries series, final long startTime, final long endTime, final boolean includeMovingAverage, - final int percentage) + final MovingAverageConfiguration movingAverageConfig) { - return createLineChart(chartTitle, rangeAxisTitle, series, startTime, endTime, includeMovingAverage, percentage, true); + return createLineChart(chartTitle, rangeAxisTitle, series, startTime, endTime, includeMovingAverage, movingAverageConfig, true); } /** @@ -692,17 +737,17 @@ public static JFreeChart createLineChart(final String chartTitle, final String r * the end time of the x-axis * @param includeMovingAverage * whether or not an additional moving average time series should be included - * @param percentage - * the percentaged amount of values for building the moving average + * @param movingAverageConfig + * the configuration for building the moving average * @param showDots * whether to additionally visualize the values as dots * @return the chart */ public static JFreeChart createLineChart(final String chartTitle, final String rangeAxisTitle, final TimeSeries series, final long startTime, final long endTime, final boolean includeMovingAverage, - final int percentage, final boolean showDots) + final MovingAverageConfiguration movingAverageConfig, final boolean showDots) { - final TimeSeries movingAverageSeries = includeMovingAverage ? createMovingAverageTimeSeries(series, percentage) : null; + final TimeSeries movingAverageSeries = includeMovingAverage ? createMovingAverageTimeSeries(series, movingAverageConfig) : null; return createLineChart(chartTitle, rangeAxisTitle, series, movingAverageSeries, startTime, endTime, showDots, ChartScale.LINEAR, -1); @@ -716,7 +761,7 @@ public static JFreeChart createLineChart(final String chartTitle, final String r * the chart title * @param rangeAxisTitle * the name of the y-axis - * @param series + * @param timeSeries * the time series to show * @param movingAverageTimeSeries * the moving average time series @@ -930,20 +975,117 @@ public static XYLineAndShapeRenderer createLineRenderer(final Color color) * * @param series * the source series + * @param movingAverageConfig + * the moving average configuration defining how the average should be calculated + * @return the "moving average" time series + */ + public static TimeSeries createMovingAverageTimeSeries(final TimeSeries series, final MovingAverageConfiguration movingAverageConfig) + { + final String resultSeriesName = series.getKey() + " Average (" + movingAverageConfig.getName() + ")"; + + return switch (movingAverageConfig.getType()) + { + case PERCENTAGE -> createMovingAverageTimeSeriesPercentage(series, movingAverageConfig.getValue(), resultSeriesName); + case TIME -> createMovingAverageTimeSeriesTime(series, movingAverageConfig.getValue(), resultSeriesName); + }; + } + + /** + * Creates a "moving average" time series from the given time series by averaging over a given percentage of data + * points. + * + * @param series + * the source series * @param percentage - * the percentaged amount of values for building the moving average - * @return the time series + * the percentage of data points for building the moving average + * @param resultSeriesName + * the name of the result series + * @return the "moving average" time series */ - public static TimeSeries createMovingAverageTimeSeries(final TimeSeries series, final int percentage) + private static TimeSeries createMovingAverageTimeSeriesPercentage(final TimeSeries series, final int percentage, + final String resultSeriesName) { - // take the last X percent of the values - final int samples = Math.max(2, series.getItemCount() * percentage / 100); + final TimeSeries result = new TimeSeries(resultSeriesName); - // derive the name from the source series - final String avgSeriesName = series.getKey() + " (Moving Average)"; + if (!series.isEmpty()) + { + // Convert percentage into absolute number of data points. Make sure percentage stays between 1% and 100%, + // and the resulting number of samples is at least 1. + final int samples = Math.max(1, series.getItemCount() * Math.clamp(percentage, 1, 100) / 100); + + // We use a custom implementation instead of the existing JFree method here, because the JFree version + // doesn't insert points in the beginning of the average series when the number of previous points from + // the source series is lower than the sample window size. + double sum = 0.0; + for (int i = 0; i < series.getItemCount(); i++) + { + sum += series.getValue(i).doubleValue(); - // return MovingAverage.createMovingAverage(series, avgSeriesName, samples, samples); - return MovingAverage.createPointMovingAverage(series, avgSeriesName, samples); + if (i < samples) + { + // Add the average over all points so far to the result series + result.add(series.getTimePeriod(i), sum / (i + 1)); + } + else + { + // Remove the value that is outside the sample window from the sum and calculate the average + sum -= series.getValue(i - samples).doubleValue(); + result.add(series.getTimePeriod(i), sum / samples); + } + } + } + + return result; + } + + /** + * Creates a "moving average" time series from the given time series by averaging over a given time interval. + * + * @param series + * the source series + * @param seconds + * the time interval in seconds for building the moving average + * @param resultSeriesName + * the name of the result series + * @return the "moving average" time series + */ + private static TimeSeries createMovingAverageTimeSeriesTime(final TimeSeries series, final int seconds, final String resultSeriesName) + { + final TimeSeries result = new TimeSeries(resultSeriesName); + + if (!series.isEmpty()) + { + // Get time interval size in milliseconds. Make sure resulting interval is at least 1 second long. Intervals + // longer than the total series time don't need to be handled as they don't affect the calculations. + final long intervalSizeInMilliseconds = Math.max(1, seconds) * 1000L; + + // Calculate averages using two pointer method. The start pointer will be increased along the way to mark + // the first data point still within the current averaging time interval. + int startPointer = 0; + double sum = 0.0; + + for (int i = 0; i < series.getItemCount(); i++) + { + // Add current value to the sum + sum += series.getValue(i).doubleValue(); + + // Determine start time of the averaging time interval for the current point + final RegularTimePeriod period = series.getTimePeriod(i); + final long intervalStartTime = period.getFirstMillisecond() - intervalSizeInMilliseconds; + + // Remove all values outside the interval from the sum and increase the start pointer accordingly + while (series.getTimePeriod(startPointer).getFirstMillisecond() <= intervalStartTime) + { + sum -= series.getValue(startPointer).doubleValue(); + startPointer++; + } + + // Add the average for the current point to the result series + result.add(period, sum / (i - startPointer + 1)); + } + } + + return result; } /** @@ -1252,8 +1394,8 @@ private static void saveImage(final BufferedImage bufferedImage, final File outp * the chart to modify * @param rangeAxisTitle * the name of the y-axis - * @param seriesCollection - * the time series collection to show + * @param seriesConfigurations + * the time series configurations to show */ public static void setAxisTimeSeriesCollection(final JFreeChart chart, final int axisIndex, final String rangeAxisTitle, final List seriesConfigurations) @@ -1333,7 +1475,7 @@ public static void setAxisTimeSeriesCollection(final JFreeChart chart, final int * series will be {@link DoubleMinMaxTimeSeriesDataItem} objects, so the minimum/maximum/count/accumulated value * properties of a {@link DoubleMinMaxValue} will still be available. * - * @param minMaxValueSet + * @param valueSet * the source min-max value set * @param timeSeriesName * the name of the time series diff --git a/src/test/java/com/xceptance/common/util/AbstractConfigurationTest.java b/src/test/java/com/xceptance/common/util/AbstractConfigurationTest.java index f46113aff..eb5adcfdc 100644 --- a/src/test/java/com/xceptance/common/util/AbstractConfigurationTest.java +++ b/src/test/java/com/xceptance/common/util/AbstractConfigurationTest.java @@ -20,6 +20,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.List; import java.util.Properties; import java.util.Set; @@ -74,6 +75,15 @@ public void init() props.setProperty("one.two.four", "testvalue2"); props.setProperty("one.two.four.one", "testValue3"); + // props with indexes for "getPropertyKeyIndexes" tests + // (Valid indexes are 3,4,6,7. Index 4 appears twice. Index 5 doesn't exist. Indexes aren't sorted.) + props.setProperty("prop.index.4.abc", "testValue1"); + props.setProperty("prop.index.7.xyz", "testValue2"); + props.setProperty("prop.index.3.abc", "testValue3"); + props.setProperty("prop.index.4.xyz", "testValue4"); + props.setProperty("prop.index.6.xyz", "testValue5"); + props.setProperty("prop.invalidIndex.abc", "testValue6"); + conf.addProperties(props); } @@ -330,6 +340,48 @@ public void testGetPropertyKeysWithPrefix() Assert.assertTrue(keys.contains("one.two.four.one")); } + @Test + public void testGetPropertyKeyIndexes() + { + final List indexes = conf.getPropertyKeyIndexes("prop.index.", 0, 10); + + // All matching indexes are returned. Duplicates aren't included. Indexes are sorted. No exceptions thrown. + Assert.assertEquals(4, indexes.size()); + Assert.assertEquals(3, indexes.get(0).intValue()); + Assert.assertEquals(4, indexes.get(1).intValue()); + Assert.assertEquals(6, indexes.get(2).intValue()); + Assert.assertEquals(7, indexes.get(3).intValue()); + } + + @Test + public void testGetPropertyKeyIndexes_nonNumericIndex() + { + final NumberFormatException exception = Assert.assertThrows(NumberFormatException.class, + () -> conf.getPropertyKeyIndexes("prop.invalidIndex.", 0, 10)); + Assert.assertEquals(String.format(AbstractConfiguration.PROPERTY_PARSING_ERROR_INVALID_INDEX_FORMAT, "abc", "prop.invalidIndex."), + exception.getMessage()); + } + + @Test + public void testGetPropertyKeyIndexes_indexTooLow() + { + // Index 3 is not within the provided boundaries of 4 to 7 + final IndexOutOfBoundsException exception = Assert.assertThrows(IndexOutOfBoundsException.class, + () -> conf.getPropertyKeyIndexes("prop.index.", 4, 7)); + Assert.assertEquals(String.format(AbstractConfiguration.PROPERTY_PARSING_ERROR_INDEX_OUT_OF_BOUNDS, 3, "prop.index.", 4, 7), + exception.getMessage()); + } + + @Test + public void testGetPropertyKeyIndexes_indexTooHigh() + { + // Index 7 is not within the provided boundaries of 3 to 6 + final IndexOutOfBoundsException exception = Assert.assertThrows(IndexOutOfBoundsException.class, + () -> conf.getPropertyKeyIndexes("prop.index.", 3, 6)); + Assert.assertEquals(String.format(AbstractConfiguration.PROPERTY_PARSING_ERROR_INDEX_OUT_OF_BOUNDS, 7, "prop.index.", 3, 6), + exception.getMessage()); + } + /** * Private helper class that simply extends AbstractConfiguration to enable instantiation. * diff --git a/src/test/java/com/xceptance/common/util/ParseUtilsTest.java b/src/test/java/com/xceptance/common/util/ParseUtilsTest.java index 24c20f3d7..730a158d5 100644 --- a/src/test/java/com/xceptance/common/util/ParseUtilsTest.java +++ b/src/test/java/com/xceptance/common/util/ParseUtilsTest.java @@ -20,11 +20,12 @@ import java.net.URL; import java.text.ParseException; -import junitparams.JUnitParamsRunner; -import junitparams.Parameters; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import util.JUnitParamsUtils; /** @@ -466,6 +467,35 @@ public void parseLongFallback_NoLong() throws ParseException Assert.assertTrue(ParseUtils.parseLong("10,aa", 10) == 10); } + @Test + public void parseIntPercentage() throws ParseException + { + Assert.assertEquals(25, ParseUtils.parseIntPercentage("25%")); + Assert.assertEquals(25, ParseUtils.parseIntPercentage("25")); + + Assert.assertEquals(25, ParseUtils.parseIntPercentage(" 25% ")); + Assert.assertEquals(25, ParseUtils.parseIntPercentage(" 25 ")); + + Assert.assertEquals(0, ParseUtils.parseIntPercentage("0%")); + Assert.assertEquals(-1, ParseUtils.parseIntPercentage("-1%")); + + Assert.assertEquals(Integer.MAX_VALUE, ParseUtils.parseIntPercentage(Integer.MAX_VALUE + "%")); + Assert.assertEquals(Integer.MAX_VALUE, ParseUtils.parseIntPercentage(String.valueOf(Integer.MAX_VALUE))); + + Assert.assertEquals(Integer.MIN_VALUE, ParseUtils.parseIntPercentage(Integer.MIN_VALUE + "%")); + Assert.assertEquals(Integer.MIN_VALUE, ParseUtils.parseIntPercentage(String.valueOf(Integer.MIN_VALUE))); + } + + @Test(expected = ParseException.class) + @Parameters(value = + { + "12.3%", "abc", "%25", "25*" + }) + public void parseIntPercentage_invalidValue(final String invalidValue) throws ParseException + { + ParseUtils.parseIntPercentage(invalidValue); + } + @Test public void parseAbsoluteOrRelative_AbsoluteIntValue() throws ParseException { diff --git a/src/test/java/com/xceptance/xlt/api/report/MovingAverageConfigurationTest.java b/src/test/java/com/xceptance/xlt/api/report/MovingAverageConfigurationTest.java new file mode 100644 index 000000000..67a69e904 --- /dev/null +++ b/src/test/java/com/xceptance/xlt/api/report/MovingAverageConfigurationTest.java @@ -0,0 +1,34 @@ +package com.xceptance.xlt.api.report; + +import org.junit.Assert; +import org.junit.Test; + +public class MovingAverageConfigurationTest +{ + @Test + public void createPercentageConfig() + { + final MovingAverageConfiguration config = MovingAverageConfiguration.createPercentageConfig(25); + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, config.getType()); + Assert.assertEquals(25, config.getValue()); + Assert.assertEquals("25%", config.getName()); + } + + @Test + public void createTimeConfig() + { + final MovingAverageConfiguration config = MovingAverageConfiguration.createTimeConfig(300); + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, config.getType()); + Assert.assertEquals(300, config.getValue()); + Assert.assertEquals("300s", config.getName()); + } + + @Test + public void createTimeConfig_withCustomName() + { + final MovingAverageConfiguration config = MovingAverageConfiguration.createTimeConfig(300, "5m"); + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, config.getType()); + Assert.assertEquals(300, config.getValue()); + Assert.assertEquals("5m", config.getName()); + } +} diff --git a/src/test/java/com/xceptance/xlt/report/ReportGeneratorConfigurationTest.java b/src/test/java/com/xceptance/xlt/report/ReportGeneratorConfigurationTest.java new file mode 100644 index 000000000..3946a71cc --- /dev/null +++ b/src/test/java/com/xceptance/xlt/report/ReportGeneratorConfigurationTest.java @@ -0,0 +1,564 @@ +package com.xceptance.xlt.report; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import com.xceptance.xlt.api.report.MovingAverageConfiguration; +import com.xceptance.xlt.api.util.XltException; +import com.xceptance.xlt.common.XltConstants; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import util.JUnitParamsUtils; + +@RunWith(JUnitParamsRunner.class) +public class ReportGeneratorConfigurationTest +{ + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + public File homeDir; + + public File configDir; + + public File propertyFile; + + @Before + public void setup() throws IOException + { + homeDir = tempFolder.getRoot(); + configDir = tempFolder.newFolder("config"); + + // Create required "mastercontroller.properties" file in config directory + new File(configDir, "mastercontroller.properties").createNewFile(); + + // Create "reportgenerator.properties" file in config directory for testing + propertyFile = new File(configDir, "reportgenerator.properties"); + propertyFile.createNewFile(); + } + + @Test + public void commonAverage_defaultValue() throws IOException + { + // If no common average is configured, the default value is returned + final MovingAverageConfiguration commonAverage = readReportGeneratorProperties().getCommonMovingAverageConfig(); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, commonAverage.getType()); + Assert.assertEquals(5, commonAverage.getValue()); + Assert.assertEquals("5%", commonAverage.getName()); + } + + @Test + public void additionalAverages_defaultValue() throws IOException + { + // If no additional averages are configured, an empty list is returned + Assert.assertEquals(0, readReportGeneratorProperties().getAdditionalMovingAverageConfigs().size()); + } + + @Test + public void commonAverage_percentage() throws IOException + { + addCommonAverageConfig("percentage", "25%"); + + final MovingAverageConfiguration commonAverage = readReportGeneratorProperties().getCommonMovingAverageConfig(); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, commonAverage.getType()); + Assert.assertEquals(25, commonAverage.getValue()); + Assert.assertEquals("25%", commonAverage.getName()); + } + + @Test + public void commonAverage_time() throws IOException + { + addCommonAverageConfig("time", "1h15m45s"); + + final MovingAverageConfiguration commonAverage = readReportGeneratorProperties().getCommonMovingAverageConfig(); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, commonAverage.getType()); + Assert.assertEquals(4545, commonAverage.getValue()); + Assert.assertEquals("1h15m45s", commonAverage.getName()); + } + + @Test + public void additionalAverages_singleAdditionalAverage() throws IOException + { + addAdditionalAverageConfig("1", "time", "15:30"); + + final List additionalAverages = readReportGeneratorProperties().getAdditionalMovingAverageConfigs(); + Assert.assertEquals(1, additionalAverages.size()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, additionalAverages.get(0).getType()); + Assert.assertEquals(930, additionalAverages.get(0).getValue()); + Assert.assertEquals("15:30", additionalAverages.get(0).getName()); + } + + @Test + public void additionalAverages_multipleAdditionalAverages() throws IOException + { + addAdditionalAverageConfig("1", "percentage", "1%"); + addAdditionalAverageConfig("2", "percentage", "25%"); + addAdditionalAverageConfig("3", "time", "1h15m45s"); + + final List additionalAverages = readReportGeneratorProperties().getAdditionalMovingAverageConfigs(); + Assert.assertEquals(3, additionalAverages.size()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, additionalAverages.get(0).getType()); + Assert.assertEquals(1, additionalAverages.get(0).getValue()); + Assert.assertEquals("1%", additionalAverages.get(0).getName()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, additionalAverages.get(1).getType()); + Assert.assertEquals(25, additionalAverages.get(1).getValue()); + Assert.assertEquals("25%", additionalAverages.get(1).getName()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, additionalAverages.get(2).getType()); + Assert.assertEquals(4545, additionalAverages.get(2).getValue()); + Assert.assertEquals("1h15m45s", additionalAverages.get(2).getName()); + } + + @Test + public void additionalAverages_maxNumberOfAdditionalAverages() throws IOException + { + // Configure the maximum allowed number of additional averages + addAdditionalAverageConfig("1", "percentage", "2%"); + addAdditionalAverageConfig("2", "time", "1h45s"); + addAdditionalAverageConfig("3", "percentage", "40"); + addAdditionalAverageConfig("4", "time", "1250"); + addAdditionalAverageConfig("5", "time", "1:15:45"); + + final List additionalAverages = readReportGeneratorProperties().getAdditionalMovingAverageConfigs(); + Assert.assertEquals(XltConstants.REPORT_CHART_MAX_ADDITIONAL_AVERAGES, additionalAverages.size()); + + // Additional average 1 + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, additionalAverages.get(0).getType()); + Assert.assertEquals(2, additionalAverages.get(0).getValue()); + Assert.assertEquals("2%", additionalAverages.get(0).getName()); + + // Additional average 2 + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, additionalAverages.get(1).getType()); + Assert.assertEquals(3645, additionalAverages.get(1).getValue()); + Assert.assertEquals("1h45s", additionalAverages.get(1).getName()); + + // Additional average 3 + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, additionalAverages.get(2).getType()); + Assert.assertEquals(40, additionalAverages.get(2).getValue()); + Assert.assertEquals("40%", additionalAverages.get(2).getName()); + + // Additional average 4 + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, additionalAverages.get(3).getType()); + Assert.assertEquals(1250, additionalAverages.get(3).getValue()); + Assert.assertEquals("1250s", additionalAverages.get(3).getName()); + + // Additional average 5 + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, additionalAverages.get(4).getType()); + Assert.assertEquals(4545, additionalAverages.get(4).getValue()); + Assert.assertEquals("1:15:45", additionalAverages.get(4).getName()); + } + + @Test + public void additionalAverages_gapsBetweenIndexes() throws IOException + { + // Add additional averages with indexes 2 and 4, skipping indexes 1 and 3 + addAdditionalAverageConfig("2", "time", "3:01"); + addAdditionalAverageConfig("4", "percentage", "25%"); + + final List additionalAverages = readReportGeneratorProperties().getAdditionalMovingAverageConfigs(); + Assert.assertEquals(2, additionalAverages.size()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, additionalAverages.get(0).getType()); + Assert.assertEquals(181, additionalAverages.get(0).getValue()); + Assert.assertEquals("3:01", additionalAverages.get(0).getName()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, additionalAverages.get(1).getType()); + Assert.assertEquals(25, additionalAverages.get(1).getValue()); + Assert.assertEquals("25%", additionalAverages.get(1).getName()); + } + + @Test + public void additionalAverages_unsortedIndexes() throws IOException + { + // Add additional averages with indexes out of order + addAdditionalAverageConfig("3", "percentage", "25%"); + addAdditionalAverageConfig("1", "percentage", "99%"); + addAdditionalAverageConfig("2", "time", "1h15m45s"); + + // Additional averages should be sorted by index + final List additionalAverages = readReportGeneratorProperties().getAdditionalMovingAverageConfigs(); + Assert.assertEquals(3, additionalAverages.size()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, additionalAverages.get(0).getType()); + Assert.assertEquals(99, additionalAverages.get(0).getValue()); + Assert.assertEquals("99%", additionalAverages.get(0).getName()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, additionalAverages.get(1).getType()); + Assert.assertEquals(4545, additionalAverages.get(1).getValue()); + Assert.assertEquals("1h15m45s", additionalAverages.get(1).getName()); + + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, additionalAverages.get(2).getType()); + Assert.assertEquals(25, additionalAverages.get(2).getValue()); + Assert.assertEquals("25%", additionalAverages.get(2).getName()); + } + + @Test + public void commonAndAdditionalAverages() throws IOException + { + // Configure common average and 2 additional averages + addAdditionalAverageConfig("4", "time", "2m30s"); + addCommonAverageConfig("percentage", "25%"); + addAdditionalAverageConfig("1", "percentage", "1"); + + final ReportGeneratorConfiguration config = readReportGeneratorProperties(); + + // Common average + final MovingAverageConfiguration commonAverage = config.getCommonMovingAverageConfig(); + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, commonAverage.getType()); + Assert.assertEquals(25, commonAverage.getValue()); + Assert.assertEquals("25%", commonAverage.getName()); + + // Additional average 1 + final List additionalAverages = config.getAdditionalMovingAverageConfigs(); + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, additionalAverages.get(0).getType()); + Assert.assertEquals(1, additionalAverages.get(0).getValue()); + Assert.assertEquals("1%", additionalAverages.get(0).getName()); + + // Additional average 2 + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.TIME, additionalAverages.get(1).getType()); + Assert.assertEquals(150, additionalAverages.get(1).getValue()); + Assert.assertEquals("2m30s", additionalAverages.get(1).getName()); + } + + @Test + public void commonAndAdditionalAverages_blankProperties() throws IOException + { + // Configure common average and 2 additional averages, all with blank types and values + addAdditionalAverageConfig("4", "", "\t"); + addCommonAverageConfig(" ", ""); + addAdditionalAverageConfig("1", " \t ", " "); + + final ReportGeneratorConfiguration config = readReportGeneratorProperties(); + + // Common average is set to the default value + final MovingAverageConfiguration commonAverage = config.getCommonMovingAverageConfig(); + Assert.assertEquals(MovingAverageConfiguration.MovingAverageType.PERCENTAGE, commonAverage.getType()); + Assert.assertEquals(5, commonAverage.getValue()); + Assert.assertEquals("5%", commonAverage.getName()); + + // Additional averages are empty + Assert.assertEquals(0, config.getAdditionalMovingAverageConfigs().size()); + } + + @Test + @Parameters(source = JUnitParamsUtils.BlankStringOrNullParamProvider.class) + public void commonAverage_incompleteConfiguration_valueIsMissingOrBlank(final String blankValueOrNull) + { + // Configure 'type' property, set 'value' property to blank value or skip it entirely + addCommonAverageType("percentage"); + if (blankValueOrNull != null) + { + addCommonAverageValue(blankValueOrNull); + } + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_PROPERTY_MISSING, getCommonAverageTypeKey(), + getCommonAverageValueKey()), + exception.getMessage()); + } + + @Test + @Parameters(source = JUnitParamsUtils.BlankStringOrNullParamProvider.class) + public void additionalAverages_incompleteConfiguration_valueIsMissingOrBlank(final String blankValueOrNull) + { + // Configure 'type' property, set 'value' property to blank value or skip it entirely + addAdditionalAverageType("1", "percentage"); + if (blankValueOrNull != null) + { + addAdditionalAverageValue("1", blankValueOrNull); + } + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_PROPERTY_MISSING, getAdditionalAverageTypeKey("1"), + getAdditionalAverageValueKey("1")), + exception.getMessage()); + } + + @Test + @Parameters(source = JUnitParamsUtils.BlankStringOrNullParamProvider.class) + public void commonAverage_incompleteConfiguration_typeIsMissingOrBlank(final String blankValueOrNull) + { + // Configure 'value' property, set 'type' property to blank value or skip it entirely + addCommonAverageValue("50"); + if (blankValueOrNull != null) + { + addCommonAverageType(blankValueOrNull); + } + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_PROPERTY_MISSING, getCommonAverageValueKey(), + getCommonAverageTypeKey()), + exception.getMessage()); + } + + @Test + @Parameters(source = JUnitParamsUtils.BlankStringOrNullParamProvider.class) + public void additionalAverages_incompleteConfiguration_typeIsMissingOrBlank(final String blankValueOrNull) + { + // Configure 'value' property, set 'type' property to blank value or skip it entirely + addAdditionalAverageValue("1", "50"); + if (blankValueOrNull != null) + { + addAdditionalAverageType("1", blankValueOrNull); + } + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_PROPERTY_MISSING, getAdditionalAverageValueKey("1"), + getAdditionalAverageTypeKey("1")), + exception.getMessage()); + } + + @Test + public void commonAverage_invalidType() + { + addCommonAverageConfig("invalidType", "25"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_TYPE_INVALID, "invalidType", + getCommonAverageTypeKey()), + exception.getMessage()); + } + + @Test + public void additionalAverages_invalidType() + { + addAdditionalAverageConfig("1", "invalidType", "25"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_TYPE_INVALID, "invalidType", + getAdditionalAverageTypeKey("1")), + exception.getMessage()); + } + + @Test + public void commonAverage_invalidPercentage() + { + addCommonAverageConfig("percentage", "invalidValue"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_INVALID_PROPERTY_VALUE_FORMAT, getCommonAverageValueKey()), + exception.getMessage()); + } + + @Test + public void additionalAverages_invalidPercentage() + { + addAdditionalAverageConfig("1", "percentage", "invalidValue"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_INVALID_PROPERTY_VALUE_FORMAT, + getAdditionalAverageValueKey("1")), + exception.getMessage()); + } + + @Test + public void commonAverage_invalidTime() + { + addCommonAverageConfig("time", "invalidValue"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_INVALID_PROPERTY_VALUE_FORMAT, getCommonAverageValueKey()), + exception.getMessage()); + } + + @Test + public void additionalAverages_invalidTime() + { + addAdditionalAverageConfig("1", "time", "invalidValue"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_INVALID_PROPERTY_VALUE_FORMAT, + getAdditionalAverageValueKey("1")), + exception.getMessage()); + } + + @Test + @Parameters(value = + { + "-1%", "0%", "101%", "2500%" + }) + public void commonAverage_percentageOutOfBounds(final String percentage) + { + addCommonAverageConfig("percentage", percentage); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_PERCENTAGE_OUT_OF_BOUNDS, getCommonAverageValueKey(), + percentage), + exception.getMessage()); + } + + @Test + @Parameters(value = + { + "-1%", "0%", "101%", "2500%" + }) + public void additionalAverages_percentageOutOfBounds(final String percentage) + { + addAdditionalAverageConfig("1", "percentage", percentage); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_PERCENTAGE_OUT_OF_BOUNDS, + getAdditionalAverageValueKey("1"), percentage), + exception.getMessage()); + } + + @Test + @Parameters(value = + { + "0", "0s", "0:00:00", "0h0m0s" + }) + public void commonAverage_timeOutOfBounds(final String time) + { + addCommonAverageConfig("time", time); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_TIME_OUT_OF_BOUNDS, getCommonAverageValueKey(), time), + exception.getMessage()); + } + + @Test + @Parameters(value = + { + "0", "0s", "0:00:00", "0h0m0s" + }) + public void additionalAverages_timeOutOfBounds(final String time) + { + addAdditionalAverageConfig("1", "time", time); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(String.format(ReportGeneratorConfiguration.ERROR_AVERAGE_TIME_OUT_OF_BOUNDS, getAdditionalAverageValueKey("1"), + time), + exception.getMessage()); + } + + @Test + public void additionalAverages_nonNumericIndex() + { + addAdditionalAverageConfig("abc", "percentage", "25%"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(ReportGeneratorConfiguration.ERROR_AVERAGE_INDEX_INVALID, exception.getMessage()); + } + + @Test + public void additionalAverages_indexTooLow() + { + addAdditionalAverageConfig("0", "percentage", "25%"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(ReportGeneratorConfiguration.ERROR_AVERAGE_INDEX_INVALID, exception.getMessage()); + } + + @Test + public void additionalAverages_indexTooHigh() + { + addAdditionalAverageConfig(String.valueOf(XltConstants.REPORT_CHART_MAX_ADDITIONAL_AVERAGES + 1), "percentage", "25%"); + final XltException exception = Assert.assertThrows(XltException.class, this::readReportGeneratorProperties); + Assert.assertEquals(ReportGeneratorConfiguration.ERROR_AVERAGE_INDEX_INVALID, exception.getMessage()); + } + + /** + * Helper method for reading the contents of the "reportgenerator.properties" test file. + */ + private ReportGeneratorConfiguration readReportGeneratorProperties() throws IOException + { + return new ReportGeneratorConfiguration(homeDir, configDir, null, null, null); + } + + /** + * Helper method to add the properties for the common average configuration with the given type and value. + */ + private void addCommonAverageConfig(final String type, final String value) + { + addCommonAverageType(type); + addCommonAverageValue(value); + } + + /** + * Helper method to add the properties for an additional average configuration with the given index, type and value. + */ + private void addAdditionalAverageConfig(final String index, final String type, final String value) + { + addAdditionalAverageType(index, type); + addAdditionalAverageValue(index, value); + } + + /** + * Helper method to add just the "type" property for the common average configuration. + */ + private void addCommonAverageType(final String type) + { + appendPropertyToFile(getCommonAverageTypeKey(), type); + } + + /** + * Helper method to add just the "value" property for the common average configuration. + */ + private void addCommonAverageValue(final String value) + { + appendPropertyToFile(getCommonAverageValueKey(), value); + } + + /** + * Helper method to add just the "type" property for an additional average configuration with the given index. + */ + private void addAdditionalAverageType(final String index, final String type) + { + appendPropertyToFile(getAdditionalAverageTypeKey(index), type); + } + + /** + * Helper method to add just the "value" property for an additional average configuration with the given index. + */ + private void addAdditionalAverageValue(final String index, final String value) + { + appendPropertyToFile(getAdditionalAverageValueKey(index), value); + } + + /** + * Helper method for writing a property to the 'reportgenerator.properties' test file. + */ + private void appendPropertyToFile(final String key, final String value) + { + try + { + Files.write(propertyFile.toPath(), List.of(key + " = " + value), StandardOpenOption.APPEND); + } + catch (final IOException e) + { + throw new RuntimeException(e); + } + } + + /** + * Get key for common average type property. + */ + private String getCommonAverageTypeKey() + { + return ReportGeneratorConfiguration.PROP_CHARTS_AVERAGE_COMMON + ReportGeneratorConfiguration.PROP_SUFFIX_TYPE; + } + + /** + * Get key for common average value property. + */ + private String getCommonAverageValueKey() + { + return ReportGeneratorConfiguration.PROP_CHARTS_AVERAGE_COMMON + ReportGeneratorConfiguration.PROP_SUFFIX_VALUE; + } + + /** + * Get key for additional average type property with the given index. + */ + private String getAdditionalAverageTypeKey(final String index) + { + return ReportGeneratorConfiguration.PROP_CHARTS_AVERAGES_ADDITIONAL + index + "." + ReportGeneratorConfiguration.PROP_SUFFIX_TYPE; + } + + /** + * Get key for additional average value property with the given index. + */ + private String getAdditionalAverageValueKey(final String index) + { + return ReportGeneratorConfiguration.PROP_CHARTS_AVERAGES_ADDITIONAL + index + "." + ReportGeneratorConfiguration.PROP_SUFFIX_VALUE; + } +} diff --git a/src/test/java/com/xceptance/xlt/report/providers/DummyReportProviderConfiguration.java b/src/test/java/com/xceptance/xlt/report/providers/DummyReportProviderConfiguration.java index 06d10c5ff..0492834eb 100644 --- a/src/test/java/com/xceptance/xlt/report/providers/DummyReportProviderConfiguration.java +++ b/src/test/java/com/xceptance/xlt/report/providers/DummyReportProviderConfiguration.java @@ -16,8 +16,10 @@ package com.xceptance.xlt.report.providers; import java.io.File; +import java.util.List; import java.util.Properties; +import com.xceptance.xlt.api.report.MovingAverageConfiguration; import com.xceptance.xlt.api.report.ReportProvider; import com.xceptance.xlt.api.report.ReportProviderConfiguration; @@ -84,9 +86,15 @@ public File getCsvDirectory() } @Override - public int getMovingAveragePercentage() + public MovingAverageConfiguration getCommonMovingAverageConfig() { - return 0; + return null; + } + + @Override + public List getAdditionalMovingAverageConfigs() + { + return null; } @Override diff --git a/src/test/java/com/xceptance/xlt/report/util/JFreeChartUtilsTest.java b/src/test/java/com/xceptance/xlt/report/util/JFreeChartUtilsTest.java index 16d9e8df9..f30b180a4 100644 --- a/src/test/java/com/xceptance/xlt/report/util/JFreeChartUtilsTest.java +++ b/src/test/java/com/xceptance/xlt/report/util/JFreeChartUtilsTest.java @@ -17,22 +17,29 @@ import java.util.Date; +import org.jfree.data.time.Minute; import org.jfree.data.time.Second; import org.jfree.data.time.TimeSeries; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import org.junit.runner.RunWith; +import com.xceptance.xlt.api.report.MovingAverageConfiguration; import com.xceptance.xlt.report.ReportGeneratorMain; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + /** * @author Sebastian Oerding */ +@RunWith(JUnitParamsRunner.class) public class JFreeChartUtilsTest { /** - * This tests {@link JFreeChartUtils#toMinMaxTimeSeries(IntMinMaxValueSet, String)} and causes an error due to a large - * scale in the MinMaxValueSet and multiplication with 1000 in the method in JFreeChartUtils. + * This tests {@link JFreeChartUtils#toMinMaxTimeSeries(IntMinMaxValueSet, String)} and causes an error due to a + * large scale in the MinMaxValueSet and multiplication with 1000 in the method in JFreeChartUtils. */ @Ignore @Test @@ -83,4 +90,386 @@ public void testNewSeconds() t.add(s1, 1.0); t.add(s2, 1.0); // fails } + + /** + * Test that moving average series is calculated correctly for a source series with no time gaps between data + * points. + */ + @Test + @Parameters(method = "provideMovingAverageConfigs") + public void createMovingAverageTimeSeries(final MovingAverageConfiguration movingAverageConfig) + { + // Prepare source series with not time gaps between data points + final TimeSeries series = new TimeSeries("Test"); + series.add(getSecond(10), 100); + series.add(getSecond(11), 200); + series.add(getSecond(12), 900); + series.add(getSecond(13), 700); + series.add(getSecond(14), 650); + series.add(getSecond(15), 300); + series.add(getSecond(16), 2200); + series.add(getSecond(17), 200); + series.add(getSecond(18), 0); + series.add(getSecond(19), 422.5); + series.add(getSecond(20), 27.5); + series.add(getSecond(21), 450); + + // Calculate average over the last 25% of values or the last 3 seconds. Since there are 12 points in total with + // no gaps, this results in the average over the last 3 points in both cases. + final TimeSeries result = JFreeChartUtils.createMovingAverageTimeSeries(series, movingAverageConfig); + + // Validate result name and size + Assert.assertEquals("Test Average (" + movingAverageConfig.getName() + ")", result.getKey()); + Assert.assertEquals(12, result.getItemCount()); + + // Time periods of the result series should match time periods of the source series + Assert.assertEquals(getSecond(10), result.getTimePeriod(0)); + Assert.assertEquals(getSecond(11), result.getTimePeriod(1)); + Assert.assertEquals(getSecond(12), result.getTimePeriod(2)); + Assert.assertEquals(getSecond(13), result.getTimePeriod(3)); + Assert.assertEquals(getSecond(14), result.getTimePeriod(4)); + Assert.assertEquals(getSecond(15), result.getTimePeriod(5)); + Assert.assertEquals(getSecond(16), result.getTimePeriod(6)); + Assert.assertEquals(getSecond(17), result.getTimePeriod(7)); + Assert.assertEquals(getSecond(18), result.getTimePeriod(8)); + Assert.assertEquals(getSecond(19), result.getTimePeriod(9)); + Assert.assertEquals(getSecond(20), result.getTimePeriod(10)); + Assert.assertEquals(getSecond(21), result.getTimePeriod(11)); + + // Validate result values are average over the last 3 points of the source series + Assert.assertEquals(100.0, result.getValue(0)); + Assert.assertEquals(150.0, result.getValue(1)); + Assert.assertEquals(400.0, result.getValue(2)); + Assert.assertEquals(600.0, result.getValue(3)); + Assert.assertEquals(750.0, result.getValue(4)); + Assert.assertEquals(550.0, result.getValue(5)); + Assert.assertEquals(1050.0, result.getValue(6)); + Assert.assertEquals(900.0, result.getValue(7)); + Assert.assertEquals(800.0, result.getValue(8)); + Assert.assertEquals(207.5, result.getValue(9)); + Assert.assertEquals(150.0, result.getValue(10)); + Assert.assertEquals(300.0, result.getValue(11)); + } + + /** + * Test percentage average is calculated correctly if there are time gaps between points in the source series. + */ + @Test + public void createMovingAverageTimeSeries_percentageAverage_gapsBetweenPoints() + { + // Prepare series with gaps between time values + final TimeSeries series = new TimeSeries("Test"); + series.add(getSecond(10), 100); + series.add(getSecond(25), 200); + series.add(getSecond(200), 900); + series.add(getSecond(500), 700); + series.add(getSecond(750), 650); + series.add(getSecond(1521), 300); + series.add(getSecond(2700), 2200); + series.add(getSecond(2701), 200); + series.add(getSecond(3600), 0); + series.add(getSecond(3601), 422.5); + + // Calculate moving average over the last 33% of values. + // In this case this means "over the last 3 data points"; time gaps between points are ignored. + final TimeSeries result = JFreeChartUtils.createMovingAverageTimeSeries(series, + MovingAverageConfiguration.createPercentageConfig(33)); + + // Validate result series name and size + Assert.assertEquals("Test Average (33%)", result.getKey()); + Assert.assertEquals(10, result.getItemCount()); + + // Time periods in result series should match time periods of the source series + Assert.assertEquals(getSecond(10), result.getTimePeriod(0)); + Assert.assertEquals(getSecond(25), result.getTimePeriod(1)); + Assert.assertEquals(getSecond(200), result.getTimePeriod(2)); + Assert.assertEquals(getSecond(500), result.getTimePeriod(3)); + Assert.assertEquals(getSecond(750), result.getTimePeriod(4)); + Assert.assertEquals(getSecond(1521), result.getTimePeriod(5)); + Assert.assertEquals(getSecond(2700), result.getTimePeriod(6)); + Assert.assertEquals(getSecond(2701), result.getTimePeriod(7)); + Assert.assertEquals(getSecond(3600), result.getTimePeriod(8)); + Assert.assertEquals(getSecond(3601), result.getTimePeriod(9)); + + // Validate result values are the averages over the last 3 data points regardless of the time between points + Assert.assertEquals(100.0, result.getValue(0)); + Assert.assertEquals(150.0, result.getValue(1)); + Assert.assertEquals(400.0, result.getValue(2)); + Assert.assertEquals(600.0, result.getValue(3)); + Assert.assertEquals(750.0, result.getValue(4)); + Assert.assertEquals(550.0, result.getValue(5)); + Assert.assertEquals(1050.0, result.getValue(6)); + Assert.assertEquals(900.0, result.getValue(7)); + Assert.assertEquals(800.0, result.getValue(8)); + Assert.assertEquals(207.5, result.getValue(9)); + } + + /** + * Test time average is calculated correctly if there are time gaps between points in the source series. + */ + @Test + public void createMovingAverageTimeSeries_timeAverage_gapsBetweenPoints() + { + // Prepare series with gaps between time values, so the number of data points in the average interval fluctuates + final TimeSeries series = new TimeSeries("Test"); + series.add(getSecond(10), 100); + series.add(getSecond(25), 200); + series.add(getSecond(200), 300); + series.add(getSecond(500), 1000); + series.add(getSecond(750), 150); + series.add(getSecond(1521), 850); + series.add(getSecond(2700), 1300); + series.add(getSecond(2701), 100); + series.add(getSecond(3600), 50); + series.add(getSecond(3601), 2); + + // Calculate moving average over the last 900 seconds (i.e. 15 minutes) + final TimeSeries result = JFreeChartUtils.createMovingAverageTimeSeries(series, MovingAverageConfiguration.createTimeConfig(900)); + + // Validate result series name and size + Assert.assertEquals("Test Average (900s)", result.getKey()); + Assert.assertEquals(10, result.getItemCount()); + + // Time periods in result series should match time periods of the source series + Assert.assertEquals(getSecond(10), result.getTimePeriod(0)); + Assert.assertEquals(getSecond(25), result.getTimePeriod(1)); + Assert.assertEquals(getSecond(200), result.getTimePeriod(2)); + Assert.assertEquals(getSecond(500), result.getTimePeriod(3)); + Assert.assertEquals(getSecond(750), result.getTimePeriod(4)); + Assert.assertEquals(getSecond(1521), result.getTimePeriod(5)); + Assert.assertEquals(getSecond(2700), result.getTimePeriod(6)); + Assert.assertEquals(getSecond(2701), result.getTimePeriod(7)); + Assert.assertEquals(getSecond(3600), result.getTimePeriod(8)); + Assert.assertEquals(getSecond(3601), result.getTimePeriod(9)); + + // Validate result values are the averages over the last X data points in the average time interval + Assert.assertEquals(100.0, result.getValue(0)); // average over 1 value + Assert.assertEquals(150.0, result.getValue(1)); // average over 2 values + Assert.assertEquals(200.0, result.getValue(2)); // average over 3 values + Assert.assertEquals(400.0, result.getValue(3)); // average over 4 values + Assert.assertEquals(350.0, result.getValue(4)); // average over 5 values + Assert.assertEquals(500.0, result.getValue(5)); // average over 2 values + Assert.assertEquals(1300.0, result.getValue(6)); // average over 1 value + Assert.assertEquals(700.0, result.getValue(7)); // average over 2 values + Assert.assertEquals(75.0, result.getValue(8)); // average over 2 values + Assert.assertEquals(26.0, result.getValue(9)); // average over 2 values + } + + /** + * Test percentage average is calculated correctly for the min percentage value or even lower values (i.e. "1%" or + * less). Values lower than the minimum of "1%" should still result in the average over 1% of values. + */ + @Test + @Parameters(value = + { + "1", "0", "-1" + }) + public void createMovingAverageTimeSeries_percentageAverage_minOrLowerValues(final int percentage) + { + // Prepare a simple series with 200 values, so the average over the last 1% of values is the average over the + // last 2 data points + final TimeSeries series = new TimeSeries("Test"); + for (int i = 0; i < 200; i++) + { + // Use iteration index as the time and value for simplicity + series.add(getSecond(i), i); + } + + // The used percentage values result in "the average over the last 1% of values". In this case, this is the + // average over the last 2 data points. + final TimeSeries result = JFreeChartUtils.createMovingAverageTimeSeries(series, + MovingAverageConfiguration.createPercentageConfig(percentage)); + + // Validate result series name and size + Assert.assertEquals("Test Average (" + percentage + "%)", result.getKey()); + Assert.assertEquals(200, result.getItemCount()); + + // Validate first point of the result series matches source series + Assert.assertEquals(getSecond(0), result.getTimePeriod(0)); + Assert.assertEquals(0.0, result.getValue(0)); + + // The remaining points of the result series should be the average over the last 2 data points + for (int i = 1; i < 200; i++) + { + Assert.assertEquals(getSecond(i), result.getTimePeriod(i)); + Assert.assertEquals(((double) (2 * i - 1)) / 2, result.getValue(i)); + } + } + + /** + * Test time average is calculated correctly for the min time interval value or even lower values (i.e. "1s" or + * less). Config values lower than the minimum of "1s" should still result in the average over a 1s interval. + */ + @Test + @Parameters(value = + { + "1", "0", "-1" + }) + public void createMovingAverageTimeSeries_timeAverage_minOrLowerValues(final int seconds) + { + // Prepare source series + final TimeSeries series = new TimeSeries("Test"); + series.add(getSecond(11), 100); + series.add(getSecond(12), 200); + series.add(getSecond(13), 900); + series.add(getSecond(15), 500); + series.add(getSecond(16), 400); + + // The used second values result in "the average over the last 1 second". Since the resolution of the source + // series is 1 second, this is the average over 1 data point, i.e. the result is identical to the source series. + final TimeSeries result = JFreeChartUtils.createMovingAverageTimeSeries(series, + MovingAverageConfiguration.createTimeConfig(seconds)); + + // Validate result series name and size + Assert.assertEquals("Test Average (" + seconds + "s)", result.getKey()); + Assert.assertEquals(5, result.getItemCount()); + + // Time periods of the result series should match time periods of the source series + Assert.assertEquals(getSecond(11), result.getTimePeriod(0)); + Assert.assertEquals(getSecond(12), result.getTimePeriod(1)); + Assert.assertEquals(getSecond(13), result.getTimePeriod(2)); + Assert.assertEquals(getSecond(15), result.getTimePeriod(3)); + Assert.assertEquals(getSecond(16), result.getTimePeriod(4)); + + // Result values match values from the source series + Assert.assertEquals(100.0, result.getValue(0)); + Assert.assertEquals(200.0, result.getValue(1)); + Assert.assertEquals(900.0, result.getValue(2)); + Assert.assertEquals(500.0, result.getValue(3)); + Assert.assertEquals(400.0, result.getValue(4)); + } + + /** + * Test moving average is calculated correctly for configurations that reach or exceed the max value (i.e. + * percentages of "100%" or higher, or times that are equal or greater than the source series runtime). In those + * cases, the result series should return the averages over all previous points in the series. + */ + @Test + @Parameters(method = "provideMovingAverageConfigsWithMaxOrHigherValues") + public void createMovingAverageTimeSeries_maxOrHigherValues(final MovingAverageConfiguration config) + { + // Prepare source series with a total time range of 3600 seconds + final TimeSeries series = new TimeSeries("Test"); + series.add(getSecond(11), 100); + series.add(getSecond(12), 200); + series.add(getSecond(100), 900); + series.add(getSecond(1500), 800); + series.add(getSecond(3610), 1000); + + // Config values match or exceed 100% or the max series runtime, so average is calculated over all points in the + // series so far + final TimeSeries result = JFreeChartUtils.createMovingAverageTimeSeries(series, config); + + // Validate result series name and size + Assert.assertEquals("Test Average (" + config.getName() + ")", result.getKey()); + Assert.assertEquals(5, result.getItemCount()); + + // Time periods of the result series should match time periods of the source series + Assert.assertEquals(getSecond(11), result.getTimePeriod(0)); + Assert.assertEquals(getSecond(12), result.getTimePeriod(1)); + Assert.assertEquals(getSecond(100), result.getTimePeriod(2)); + Assert.assertEquals(getSecond(1500), result.getTimePeriod(3)); + Assert.assertEquals(getSecond(3610), result.getTimePeriod(4)); + + // Result values are average over all values in the series so far + Assert.assertEquals(100.0, result.getValue(0)); // average over 1 value + Assert.assertEquals(150.0, result.getValue(1)); // average over 2 values + Assert.assertEquals(400.0, result.getValue(2)); // average over 3 values + Assert.assertEquals(500.0, result.getValue(3)); // average over 4 values + Assert.assertEquals(600.0, result.getValue(4)); // average over 5 values + } + + /** + * Test time average is calculated correctly if the time period setting of the source series is "Minute" instead of + * "Second". + */ + @Test + public void createMovingAverageTimeSeries_timeAverage_seriesTimePeriodSettingIsMinute() + { + // Prepare source series with a "Minute" time period setting instead of "Second" + final TimeSeries series = new TimeSeries("Test"); + series.add(getMinute(2), 100); + series.add(getMinute(3), 200); + series.add(getMinute(4), 900); + series.add(getMinute(7), 700); + series.add(getMinute(9), 600); + + // Calculate average over the last 121 seconds (i.e. 2 minutes and 1 second) + final TimeSeries result = JFreeChartUtils.createMovingAverageTimeSeries(series, MovingAverageConfiguration.createTimeConfig(121)); + + // Time periods of the result series should match time periods of the source series + Assert.assertEquals(getMinute(2), result.getTimePeriod(0)); + Assert.assertEquals(getMinute(3), result.getTimePeriod(1)); + Assert.assertEquals(getMinute(4), result.getTimePeriod(2)); + Assert.assertEquals(getMinute(7), result.getTimePeriod(3)); + Assert.assertEquals(getMinute(9), result.getTimePeriod(4)); + + // Result values are average over all values in the time interval + Assert.assertEquals(100.0, result.getValue(0)); // average over 1 value + Assert.assertEquals(150.0, result.getValue(1)); // average over 2 values + Assert.assertEquals(400.0, result.getValue(2)); // average over 3 values + Assert.assertEquals(700.0, result.getValue(3)); // average over 1 values + Assert.assertEquals(650.0, result.getValue(4)); // average over 2 values + } + + /** + * Test moving average calculation returns an empty result series if the source series is empty. + */ + @Test + @Parameters(method = "provideMovingAverageConfigs") + public void createMovingAverageTimeSeries_emptySeries(final MovingAverageConfiguration config) + { + final TimeSeries result = JFreeChartUtils.createMovingAverageTimeSeries(new TimeSeries("Test"), config); + Assert.assertEquals("Test Average (" + config.getName() + ")", result.getKey()); + Assert.assertEquals(0, result.getItemCount()); + } + + /** + * Helper method to get a "Second" object for the given second number. + */ + private Second getSecond(final int second) + { + return new Second(new Date(second * 1000L)); + } + + /** + * Helper method to get a "Minute" object for the given minute number. + */ + private Minute getMinute(final int minute) + { + return new Minute(new Date(minute * 60L * 1000L)); + } + + /** + * Provides simple moving average configurations. + */ + @SuppressWarnings("unused") + private Object[] provideMovingAverageConfigs() + { + return new Object[] + { + MovingAverageConfiguration.createPercentageConfig(25), MovingAverageConfiguration.createTimeConfig(3) + }; + } + + /** + * Provides moving average configurations that reach or exceed the max possible values (i.e. percentages of "100%" + * or higher, or times that match or exceed the total time range of the source series). This method assumes a total + * source series time range of 3600s and should be used in tests accordingly. + */ + @SuppressWarnings("unused") + private Object[] provideMovingAverageConfigsWithMaxOrHigherValues() + { + return new Object[] + { + // percentage averages set to 100% or more + MovingAverageConfiguration.createPercentageConfig(100), // at the limit + MovingAverageConfiguration.createPercentageConfig(101), // above the limit + MovingAverageConfiguration.createPercentageConfig(Integer.MAX_VALUE), // max value + // time averages set to 3600s or more + MovingAverageConfiguration.createTimeConfig(3600), // at the limit + MovingAverageConfiguration.createTimeConfig(3601), // above the limit + MovingAverageConfiguration.createTimeConfig(Integer.MAX_VALUE) // max value + }; + } }