+
+
+
+
\ No newline at end of file
diff --git a/config/xsl/diffreport/text/descriptions.xsl b/config/xsl/diffreport/text/descriptions.xsl
index 7517711bc..e541922d2 100644
--- a/config/xsl/diffreport/text/descriptions.xsl
+++ b/config/xsl/diffreport/text/descriptions.xsl
@@ -168,6 +168,90 @@
+
+
+
Web Vitals
+
+
+
+
+
+ The Web Vitals section helps you evaluate how well your pages perform on aspects that are important for a great user experience.
+
+
+
+
+
+
+ Web Vitals is
+ a Google initiative to provide unified guidance for web page
+ quality signals that are essential to delivering a great
+ user experience on the web. It aims to simplify the wide
+ variety of available performance-measuring tools, and help
+ site owners focus on the metrics that matter most, the Core
+ Web Vitals.
+
+
Core Web Vitals
+
+
+ Largest
+ Contentful Paint · Perceived Load
+ Speed LCP reports the render time of the
+ largest image or text block visible in the viewport,
+ relative to when the user first navigated to the page.
+
+
+ First Input
+ Delay · Interactivity FID measures the
+ time from when a user first interacts with a page (that is,
+ when they click a link, tap on a button, or use a custom,
+ JavaScript-powered control) to the time when the browser is
+ actually able to begin processing event handlers in response
+ to that interaction.
+
+
+ Cumulative
+ Layout Shift · Visual Stability CLS is a
+ measure of the largest burst of layout shift scores for
+ every unexpected layout shift that occurs during the
+ lifespan of a page.
+
+
Other Web Vitals
+
+
+ First
+ Contentful Paint · Perceived Load
+ Speed The FCP metric measures the time from
+ when the user first navigated to the page to when any part
+ of the page's content is rendered on the screen.
+
+
+ Interaction to
+ Next Paint · Interactivity INP is a
+ metric that assesses a page's overall responsiveness to user
+ interactions by observing the latency of all click, tap, and
+ keyboard interactions that occur throughout the lifespan of
+ a user's visit to a page. The final INP value is the longest
+ interaction observed, ignoring outliers.
+
+
+ Time to First
+ Byte · Server Responsiveness TTFB is a
+ metric that measures the time between the request for a
+ resource and when the first byte of a response begins to
+ arrive.
+
+
Scores
+
+
+ The displayed score value for a Web Vital is the 75th
+ percentile (estimated) of all measurements in a given
+ action.
+
+
+
+
+
Custom Timers
@@ -181,6 +265,19 @@
+
+
+
Custom Values
+
+
+
+
+ The custom values include all values that have been recorded by your custom samplers.
+
diff --git a/resultbrowser/src/js/resultbrowser.js b/resultbrowser/src/js/resultbrowser.js
index 58cfa60b5..1b2f571e5 100644
--- a/resultbrowser/src/js/resultbrowser.js
+++ b/resultbrowser/src/js/resultbrowser.js
@@ -5,6 +5,8 @@
requestContent = null,
requestText = null,
actionContent = null,
+ actionContentImage = null,
+ actionImage = null,
errorContent = null,
postRequestParam = null,
requestBodySmall = null,
@@ -19,8 +21,10 @@
},
menu = null,
menuIcon = null,
+ menuClose = null,
transactionContent = null,
- valueLog = null;
+ valueLog = null,
+ zoomLevel = 100;
// function aliases
const getElementById = id => document.getElementById(id),
@@ -127,6 +131,8 @@
requestContent = getElementById("requestcontent");
requestText = getElementById("requesttext");
actionContent = getElementById("actioncontent");
+ actionContentImage = getElementById("actioncontent_image");
+ actionImage = getElementById("actionimage");
errorContent = getElementById("errorcontent");
postRequestParam = getElementById("postrequestparameters");
requestBodySmall = getElementById("requestBodySmall");
@@ -139,6 +145,7 @@
menu = getElementById("menu");
menuIcon = getElementById("menu-icon");
+ menuClose = getElementById("menu-close");
extras.highlight = !!window.hljs;
extras.beautify.js = !!window.js_beautify;
@@ -211,6 +218,11 @@
}
setText(requestText, s);
});
+
+ const actionimgSwitch = getElementById("actionimgSwitch");
+ actionimgSwitch.addEventListener("click", function () {
+ zoomImage(actionimgSwitch);
+ });
}
const selectResponseContent = getElementById("selectResponseContent");
@@ -224,6 +236,7 @@
// menu button
menuIcon.addEventListener("click", showMenu);
+ menuClose.addEventListener("click", showMenu);
document.addEventListener("click", function (e) {
const x = e.target;
if (filterElements(getParents(x), parent => parent.id == "menu").length === 0 && x.id != "menu-icon") {
@@ -254,6 +267,28 @@
// transaction page
transaction.addEventListener("click", showTransaction);
}
+
+ function zoomImage(element) {
+
+ if (element.id == "actionimgSwitch")
+ {
+ if (element.classList.contains('fit'))
+ {
+ element.classList.remove('fit');
+ element.classList.add('full');
+ element.title = "Click to enlarge to 100%";
+ actionImage.style = "width: 100%;"
+ }
+ else if (element.classList.contains('full'))
+ {
+ element.classList.remove('full');
+ element.classList.add('fit');
+ element.title = "Click to fit image to screen width";
+ actionImage.style = "size: 100%;"
+ zoomLevel = 100;
+ }
+ }
+ }
function toggleContent(element) {
// show the given content pane and hide the others
@@ -294,8 +329,18 @@
const data = dataStore.fetchData(element),
actionFile = data.fileName;
if (actionFile) {
- actionContent.setAttribute("src", actionFile);
- toggleContent(actionContent);
+ if (actionFile.indexOf(".png") > -1)
+ {
+ // put page screenshots in div
+ actionImage.setAttribute("src", actionFile);
+ toggleContent(actionContentImage);
+ }
+ else
+ {
+ // put html pages in iframe
+ actionContent.setAttribute("src", actionFile);
+ toggleContent(actionContent);
+ }
}
else {
toggleContent(errorContent);
@@ -307,6 +352,14 @@
element.classList.add("current");
}
}
+
+ function showHideMenu() {
+ // show/hide the menu
+ document.getElementById("leftSideMenu").classList.toggle("expanded");
+ document.getElementById("content").classList.toggle("expanded");
+ // switch the menu toggle
+ document.getElementById("mExpander").classList.toggle("expanded");
+ }
function expandCollapseAction(element) {
// lazily create the requests
@@ -894,10 +947,23 @@
hide(menu);
}
else {
+
menu.style.position = "absolute";
- menu.style.top = `${menuIcon.offsetTop}px`;
- menu.style.left = `${menuIcon.offsetLeft + 17}px`;
menu.style.zIndex = "200001";
+ var mm = window.matchMedia("(max-width: 599px), (max-height: 400px) ")
+ if (mm.matches)
+ {
+ //mobile: use full screen for menu overlay
+ menu.style.top = `0px`;
+ menu.style.left = `0px`;
+ }
+ else
+ {
+ //desktop: pop up menu next to icon
+ menu.style.top = `${menuIcon.offsetTop}px`;
+ menu.style.left = `${menuIcon.offsetLeft + 17}px`;
+ }
+
show(menu);
}
@@ -1002,11 +1068,58 @@
activateTab(this);
}));
- Split(['#leftSideMenu', '#content'], {
- sizes: [15, 85],
- minSize: [300, 600],
- gutterSize: 3
- })
+ function splitContent(mm) {
+ if (mm.matches) { // If media query matches / is large screen
+ Split(['#leftSideMenu', '#content'], {
+ sizes: [15, 85],
+ minSize: [300, 600],
+ gutterSize: 3
+ });
+ }
+ else { //on small screens/mobile: revert changes made by Split
+ document.getElementById("leftSideMenu").removeAttribute("style");
+ document.getElementById("content").removeAttribute("style");
+ var gutter = document.getElementsByClassName('gutter-horizontal');
+ while(gutter[0]) {
+ gutter[0].parentNode.removeChild(gutter[0]);
+ }
+
+ if (document.getElementById("mExpander") === null)
+ {
+ const mExpander = document.createElement("span");
+ mExpander.classList.add("expander");
+ mExpander.classList.add("expanded");
+ mExpander.id = "mExpander";
+ mExpander.title = "Single-click to show/hide menu.";
+ mExpander.textContent = "☰";
+ document.getElementById("header").insertBefore(mExpander, document.getElementById("header").firstChild);
+
+ // setup click to show/hide requests
+ mExpander.addEventListener(
+ "click",
+ function () {
+ showHideMenu();
+ }
+ );
+
+ // content click also shows/hides menu
+ /*content.addEventListener(
+ "click",
+ function () {
+ showHideMenu();
+ }
+ );*/
+ }
+ }
+ }
+ // Create a MediaQueryList object
+ var mm = window.matchMedia("(min-width: 600px)")
+ // Call listener function at run time
+ splitContent(mm);
+ // Attach listener function on state changes
+ mm.addEventListener("change", function() {
+ splitContent(mm);
+ });
// activate first request-tab
activateTab(requestContent.querySelector(".tabs-nav li"));
diff --git a/samples/app-server/start.ini b/samples/app-server/start.ini
index eb27b65aa..598f2643d 100644
--- a/samples/app-server/start.ini
+++ b/samples/app-server/start.ini
@@ -33,10 +33,6 @@ etc/xc-loginservice.xml
# Property Overrides
#===========================================================
-# use XC keystore/truststore
-jetty.sslContext.keyStorePath=etc/xc-keystore.jks
-jetty.sslContext.trustStorePath=etc/xc-keystore.jks
-
## the address to which the server will be bound
#jetty.http.host=0.0.0.0
#jetty.ssl.host=${jetty.http.host}
@@ -45,3 +41,18 @@ jetty.sslContext.trustStorePath=etc/xc-keystore.jks
jetty.http.port=8080
jetty.ssl.port=8443
jetty.httpConfig.securePort=${jetty.ssl.port}
+
+## use default XC keystore/truststore
+jetty.sslContext.keyStorePath=etc/xc-keystore.jks
+jetty.sslContext.trustStorePath=etc/xc-keystore.jks
+
+## alternative keystore/truststore for testing mTLS
+#jetty.sslContext.keyStorePath=_mtls/server-keystore.jks
+#jetty.sslContext.keyStorePassword=
+#jetty.sslContext.keyManagerPassword=
+
+#jetty.sslContext.trustStorePath=_mtls/server-truststore.jks
+#jetty.sslContext.trustStorePassword=
+
+#jetty.sslContext.needClientAuth=true
+#jetty.sslContext.wantClientAuth=true
diff --git a/samples/testsuite-performance/config/default.properties b/samples/testsuite-performance/config/default.properties
index 7e1c79c58..31ef83fe4 100644
--- a/samples/testsuite-performance/config/default.properties
+++ b/samples/testsuite-performance/config/default.properties
@@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true
## need to be enabled there again before they can be used here.
#com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2
+## The path to a Java key store file with a client key/certificate.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well. Please ensure that the key in the store is always protected by the
+## same password as the store itself.
+#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12
+#com.xceptance.xlt.tls.keyStore.password =
+
+## The path to a Java key store file with trusted server certificates.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well.
+## If you don't configure any trust store here, the system-default trust store
+## will be used.
+#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12
+#com.xceptance.xlt.tls.trustStore.password =
+
## The connection timeout [ms] on sockets and establishing the connection. This
## value is used therefore twice. So a value of 30000 will set a timeout of
## 30 seconds to establish the connection and 30 seconds on waiting for data.
diff --git a/samples/testsuite-posters/config/default.properties b/samples/testsuite-posters/config/default.properties
index 0a7e7c6c9..ab7173015 100644
--- a/samples/testsuite-posters/config/default.properties
+++ b/samples/testsuite-posters/config/default.properties
@@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true
## need to be enabled there again before they can be used here.
#com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2
+## The path to a Java key store file with a client key/certificate.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well. Please ensure that the key in the store is always protected by the
+## same password as the store itself.
+#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12
+#com.xceptance.xlt.tls.keyStore.password =
+
+## The path to a Java key store file with trusted server certificates.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well.
+## If you don't configure any trust store here, the system-default trust store
+## will be used.
+#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12
+#com.xceptance.xlt.tls.trustStore.password =
+
## The connection timeout [ms] on sockets and establishing the connection. This
## value is used therefore twice. So a value of 30000 will set a timeout of
## 30 seconds to establish the connection and 30 seconds on waiting for data.
diff --git a/samples/testsuite-showcases/config/default.properties b/samples/testsuite-showcases/config/default.properties
index 0ae674170..34a629475 100644
--- a/samples/testsuite-showcases/config/default.properties
+++ b/samples/testsuite-showcases/config/default.properties
@@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true
## need to be enabled there again before they can be used here.
#com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2
+## The path to a Java key store file with a client key/certificate.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well. Please ensure that the key in the store is always protected by the
+## same password as the store itself.
+#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12
+#com.xceptance.xlt.tls.keyStore.password =
+
+## The path to a Java key store file with trusted server certificates.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well.
+## If you don't configure any trust store here, the system-default trust store
+## will be used.
+#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12
+#com.xceptance.xlt.tls.trustStore.password =
+
## The connection timeout [ms] on sockets and establishing the connection. This
## value is used therefore twice. So a value of 30000 will set a timeout of
## 30 seconds to establish the connection and 30 seconds on waiting for data.
diff --git a/samples/testsuite-template/config/default.properties b/samples/testsuite-template/config/default.properties
index 7e1c79c58..31ef83fe4 100644
--- a/samples/testsuite-template/config/default.properties
+++ b/samples/testsuite-template/config/default.properties
@@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true
## need to be enabled there again before they can be used here.
#com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2
+## The path to a Java key store file with a client key/certificate.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well. Please ensure that the key in the store is always protected by the
+## same password as the store itself.
+#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12
+#com.xceptance.xlt.tls.keyStore.password =
+
+## The path to a Java key store file with trusted server certificates.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well.
+## If you don't configure any trust store here, the system-default trust store
+## will be used.
+#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12
+#com.xceptance.xlt.tls.trustStore.password =
+
## The connection timeout [ms] on sockets and establishing the connection. This
## value is used therefore twice. So a value of 30000 will set a timeout of
## 30 seconds to establish the connection and 30 seconds on waiting for data.
diff --git a/samples/testsuite-xlt/config/default.properties b/samples/testsuite-xlt/config/default.properties
index 0ae674170..34a629475 100644
--- a/samples/testsuite-xlt/config/default.properties
+++ b/samples/testsuite-xlt/config/default.properties
@@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true
## need to be enabled there again before they can be used here.
#com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2
+## The path to a Java key store file with a client key/certificate.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well. Please ensure that the key in the store is always protected by the
+## same password as the store itself.
+#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12
+#com.xceptance.xlt.tls.keyStore.password =
+
+## The path to a Java key store file with trusted server certificates.
+## Specify the path relative to the root directory of the test suite.
+## If the store is protected by a password, you can configure it here
+## as well.
+## If you don't configure any trust store here, the system-default trust store
+## will be used.
+#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12
+#com.xceptance.xlt.tls.trustStore.password =
+
## The connection timeout [ms] on sockets and establishing the connection. This
## value is used therefore twice. So a value of 30000 will set a timeout of
## 30 seconds to establish the connection and 30 seconds on waiting for data.
diff --git a/src/main/java/com/xceptance/xlt/common/XltPropertyNames.java b/src/main/java/com/xceptance/xlt/common/XltPropertyNames.java
index 65e841021..e4f79561f 100644
--- a/src/main/java/com/xceptance/xlt/common/XltPropertyNames.java
+++ b/src/main/java/com/xceptance/xlt/common/XltPropertyNames.java
@@ -36,6 +36,8 @@ public static class Errors
public static final String DIRECTORY_LIMIT_PER_ERROR = BASE + "directoryLimitPerError";
public static final String DIRECTORY_REPLACEMENT_CHANCE = BASE + "directoryReplacementChance";
+
+ public static final String STACKTRACES_LIMIT = BASE + "stackTracesLimit";
}
}
}
diff --git a/src/main/java/com/xceptance/xlt/engine/XltWebClient.java b/src/main/java/com/xceptance/xlt/engine/XltWebClient.java
index 2e36c97cf..49c0f10be 100644
--- a/src/main/java/com/xceptance/xlt/engine/XltWebClient.java
+++ b/src/main/java/com/xceptance/xlt/engine/XltWebClient.java
@@ -15,10 +15,12 @@
*/
package com.xceptance.xlt.engine;
+import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
+import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -33,6 +35,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.htmlunit.AjaxController;
@@ -81,6 +84,7 @@
import com.xceptance.xlt.api.engine.SessionShutdownListener;
import com.xceptance.xlt.api.htmlunit.LightWeightPage;
import com.xceptance.xlt.api.util.ResponseProcessor;
+import com.xceptance.xlt.api.util.XltException;
import com.xceptance.xlt.api.util.XltLogger;
import com.xceptance.xlt.api.util.XltProperties;
import com.xceptance.xlt.engine.htmlunit.apache.XltApacheHttpWebConnection;
@@ -127,10 +131,15 @@ public class XltWebClient extends WebClient implements SessionShutdownListener,
*/
private static final XltCache globalCache;
+ /**
+ * The global cache for key and trust stores. The cache is shared among all {@link XltWebClient} instances.
+ */
+ private static final ConcurrentHashMap storeCache;
+
/**
* Holds the URLs and responses of static resources that have been loaded so far for a page.
*/
- private final ConcurrentHashMap pageLocalCache = new ConcurrentHashMap();
+ private final ConcurrentHashMap pageLocalCache = new ConcurrentHashMap<>();
/**
* Enabled flag for static content loading.
@@ -166,7 +175,7 @@ public class XltWebClient extends WebClient implements SessionShutdownListener,
/**
* The list of response processors.
*/
- private final List responseProcessors = new ArrayList();
+ private final List responseProcessors = new ArrayList<>();
/**
* JS Beautifying response processor (separate instance field since its (omni)presence is configuration-dependent).
@@ -181,7 +190,7 @@ public class XltWebClient extends WebClient implements SessionShutdownListener,
/**
* Mapping of base URLs to relative CSS resource URLs.
*/
- private final ConcurrentHashMap> cssResourceUrlCache = new ConcurrentHashMap>();
+ private final ConcurrentHashMap> cssResourceUrlCache = new ConcurrentHashMap<>();
/**
* The maximum number of download threads.
@@ -224,6 +233,7 @@ public class XltWebClient extends WebClient implements SessionShutdownListener,
cssCacheSize = Math.max(cssCacheSize, ConcurrentLRUCache.MIN_SIZE);
globalCache = new XltCache(jsCacheSize, cssCacheSize);
+ storeCache = new ConcurrentHashMap<>();
// configure the XPath engine to use
final String xpathEngine = props.getProperty("com.xceptance.xlt.xpath.engine", "jaxen");
@@ -404,7 +414,7 @@ public XltWebClient(final BrowserVersion browserVersion, final boolean javaScrip
getOptions().setUseInsecureSSL(props.getProperty("com.xceptance.xlt.ssl.easyMode", false));
// the SSL protocol (family) to use when in easy mode
- String easyModeProtocol = StringUtils.defaultIfBlank(props.getProperty("com.xceptance.xlt.ssl.easyModeProtocol"), "TLS");
+ final String easyModeProtocol = StringUtils.defaultIfBlank(props.getProperty("com.xceptance.xlt.ssl.easyModeProtocol"), "TLS");
getOptions().setSSLInsecureProtocol(easyModeProtocol.trim());
// the SSL handshake protocols to enable at an SSL socket
@@ -464,6 +474,10 @@ public XltWebClient(final BrowserVersion browserVersion, final boolean javaScrip
final XltHttpWebConnection connection = new XltHttpWebConnection(this, underlyingWebConnection);
setWebConnection(connection);
+
+ // load key/trust material for client/server authentication
+ setUpKeyStore(props);
+ setUpTrustStore(props);
}
/**
@@ -683,7 +697,7 @@ public void loadNewStaticContent(final HtmlPage htmlPage)
final RequestStack requestStack = RequestStack.getCurrent();
requestStack.setTimerName(getTimerName());
- final Set urlStrings = new TreeSet();
+ final Set urlStrings = new TreeSet<>();
// No need to select img elements too -> they are automatically downloaded (#1379)
// Scripts can be skipped as well -> automatically downloaded as needed
elements = htmlPage.getByXPath("//input[@type='image']");
@@ -743,7 +757,7 @@ public void loadNewStaticContent(final HtmlPage htmlPage)
}
// use of copy of the frame list to minimize the chance of ConcurrentModificationException's (#1676)
- final List frames = new ArrayList(htmlPage.getFrames());
+ final List frames = new ArrayList<>(htmlPage.getFrames());
for (final WebWindow frame : frames)
{
final Page framePage = frame.getEnclosedPage();
@@ -787,7 +801,7 @@ private void loadStaticContent(final WebResponse response)
// use a sorted set to hold the links -> this way each resource will be
// loaded only once and in the same order
- final Set urlStrings = new TreeSet();
+ final Set urlStrings = new TreeSet<>();
final boolean isCssModeAlways = CssMode.ALWAYS.equals(getCssMode());
@@ -1073,7 +1087,7 @@ private static Collection resolveUrls(final Collection urlStrings,
{
// use a hash map to avoid URL duplicates since given collection of URL strings may also contain absolute URL
// strings
- final HashMap resolvedURLs = new HashMap(urlStrings.size());
+ final HashMap resolvedURLs = new HashMap<>(urlStrings.size());
for (final String urlString : urlStrings)
{
final URL absoluteURL = makeUrlAbsolute(baseURL, urlString);
@@ -1097,7 +1111,7 @@ private static Collection resolveUrls(final Collection urlStrings,
*/
private List getAllowedLinkURIs(final String pageContent)
{
- final List links = new ArrayList();
+ final List links = new ArrayList<>();
final String relAttRegex = loadStaticContent ? LINKTYPE_WHITELIST_PATTERN : "(?i)stylesheet";
for (final String attributeList : LWPageUtilities.getAllLinkAttributes(pageContent))
{
@@ -1447,8 +1461,8 @@ private void evalCss(final HtmlPage page, final URL baseURL)
evalCss(body, cssRulesWithUrls, browserVersion, cssText);
// create absolute URLs
- final Collection urls = new ArrayList();
- final Set alreadyLoadedUrls = new HashSet(pageLocalCache.keySet());
+ final Collection urls = new ArrayList<>();
+ final Set alreadyLoadedUrls = new HashSet<>(pageLocalCache.keySet());
final List pageBaseURLs = Arrays.asList(baseURL);
for (final String urlString : CssUtils.getUrlStrings(cssText.toString()))
@@ -1486,7 +1500,7 @@ private void evalCss(final HtmlPage page, final URL baseURL)
*/
private List getCssBaseUrls(final String urlString)
{
- final List urls = new ArrayList();
+ final List urls = new ArrayList<>();
for (final Map.Entry> entry : cssResourceUrlCache.entrySet())
{
if (entry.getValue().contains(urlString))
@@ -1579,7 +1593,7 @@ private void evalCss(final HtmlElement element, final List css
*/
private List getCssRulesWithUrls(final HtmlPage page)
{
- final List cssRules = new ArrayList();
+ final List cssRules = new ArrayList<>();
if (isJavaScriptEnabled())
{
@@ -1685,7 +1699,7 @@ else if (rule instanceof CSSMediaRuleImpl)
{
// get the embedded CSS rules and process them recursively
final List rules = mediaRule.getCssRules().getRules();
- for (AbstractCSSRuleImpl r : rules)
+ for (final AbstractCSSRuleImpl r : rules)
{
addCssStyleRulesWithUrls(sheet, r, cssStyleRules);
}
@@ -1714,7 +1728,7 @@ private static boolean containsUrl(final String cssText)
*/
private List getAllWebWindows(final Page page)
{
- final List webWindows = new ArrayList();
+ final List webWindows = new ArrayList<>();
if (page != null)
{
@@ -1951,20 +1965,20 @@ else if (browserType.equals("FF_ESR"))
enum CssMode
{
- /**
- * Always download images references in CSS files.
- */
- ALWAYS,
+ /**
+ * Always download images references in CSS files.
+ */
+ ALWAYS,
- /**
- * Resolve and download image references on demand.
- */
- ONDEMAND,
+ /**
+ * Resolve and download image references on demand.
+ */
+ ONDEMAND,
- /**
- * Never resolve or download any image reference.
- */
- NEVER;
+ /**
+ * Never resolve or download any image reference.
+ */
+ NEVER;
/**
* Returns the CSSMode instance for the given mode string.
@@ -1995,25 +2009,25 @@ else if (s.equals("ondemand"))
enum AjaxMode
{
- /**
- * Perform AJAX calls always asynchronously.
- */
- ASYNC,
-
- /**
- * Perform AJAX calls always synchronously.
- */
- SYNC,
-
- /**
- * Perform AJAX calls as intended by programmer.
- */
- NORMAL,
-
- /**
- * Re-synchronize asynchronous AJAX calls calling from the main thread.
- */
- RESYNC;
+ /**
+ * Perform AJAX calls always asynchronously.
+ */
+ ASYNC,
+
+ /**
+ * Perform AJAX calls always synchronously.
+ */
+ SYNC,
+
+ /**
+ * Perform AJAX calls as intended by programmer.
+ */
+ NORMAL,
+
+ /**
+ * Re-synchronize asynchronous AJAX calls calling from the main thread.
+ */
+ RESYNC;
/**
* Returns the AjaxMode instance for the given mode string.
@@ -2044,4 +2058,100 @@ else if (s.equals("resync"))
return mode;
}
}
+
+ /**
+ * Sets up the key store with the client key to be used by this web client. The store is read from a file specified
+ * in the test suite configuration.
+ *
+ * @param props
+ * the configuration
+ */
+ private void setUpKeyStore(final XltProperties props)
+ {
+ final String storeFilePath = props.getProperty("com.xceptance.xlt.tls.keyStore.file");
+ if (StringUtils.isNotBlank(storeFilePath))
+ {
+ final String storePassword = props.getProperty("com.xceptance.xlt.tls.keyStore.password");
+ final char[] storePasswordChars = getPasswordChars(storePassword);
+
+ final KeyStore store = getStore(storeFilePath, storePasswordChars);
+ getOptions().setSSLClientCertificateKeyStore(store, storePasswordChars);
+ }
+ }
+
+ /**
+ * Sets up the trust store with the server certificate to be used by this web client. The store is read from a file
+ * specified in the test suite configuration.
+ *
+ * @param props
+ * the configuration
+ */
+ private void setUpTrustStore(final XltProperties props)
+ {
+ final String storeFilePath = props.getProperty("com.xceptance.xlt.tls.trustStore.file");
+ if (StringUtils.isNotBlank(storeFilePath))
+ {
+ final String storePassword = props.getProperty("com.xceptance.xlt.tls.trustStore.password");
+ final char[] storePasswordChars = getPasswordChars(storePassword);
+
+ final KeyStore store = getStore(storeFilePath, storePasswordChars);
+ getOptions().setSSLTrustStore(store);
+ }
+ }
+
+ /**
+ * Returns the given password string as a char array.
+ *
+ * @param password
+ * the password
+ * @return the password as char array (will be empty if the password was null)
+ */
+ private static char[] getPasswordChars(final String password)
+ {
+ return password == null ? ArrayUtils.EMPTY_CHAR_ARRAY : password.toCharArray();
+ }
+
+ /**
+ * Returns the key/trust store for the given file path from the internal store cache. If the store has not been
+ * loaded so far, it will be loaded and put in the cache.
+ *
+ * @param storeFilePath
+ * the path to the store file, relative to the test suite root directory
+ * @param storePassword
+ * the store password
+ * @return the store
+ */
+ private static KeyStore getStore(final String storeFilePath, final char[] storePassword)
+ {
+ XltLogger.runTimeLogger.debug("Getting store for path '{}'", storeFilePath);
+
+ return storeCache.computeIfAbsent(storeFilePath, path -> loadStore(path, storePassword));
+ }
+
+ /**
+ * Loads a key/trust store from the given path and opens it with the provided password.
+ *
+ * @param storeFilePath
+ * the path to the store file, relative to the test suite root directory
+ * @param storePassword
+ * the store password
+ * @return the loaded store
+ * @throws XltException
+ * if anything went wrong
+ */
+ private static KeyStore loadStore(final String storeFilePath, final char[] storePassword)
+ {
+ try
+ {
+ XltLogger.runTimeLogger.debug("Loading store from '{}'", storeFilePath);
+
+ final File storeFile = new File(storeFilePath);
+
+ return KeyStore.getInstance(storeFile, storePassword);
+ }
+ catch (final Exception e)
+ {
+ throw new XltException("Failed to read key/trust store from: " + storeFilePath, e);
+ }
+ }
}
diff --git a/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java b/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java
index 94db7eec7..9201f5994 100644
--- a/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java
+++ b/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java
@@ -280,6 +280,8 @@ public enum ChartCappingMode
private final int directoryLimitPerError;
private final double directoryReplacementChance;
+
+ private final int stackTracesLimit;
private final Map apdexThresholdsByActionNamePattern = new HashMap<>();
@@ -407,6 +409,8 @@ public ReportGeneratorConfiguration(Properties xltProperties, final File overrid
directoryLimitPerError = getIntProperty(XltPropertyNames.ReportGenerator.Errors.DIRECTORY_LIMIT_PER_ERROR, 10);
directoryReplacementChance = getDoubleProperty(XltPropertyNames.ReportGenerator.Errors.DIRECTORY_REPLACEMENT_CHANCE, 0.1);
+ stackTracesLimit = getIntProperty(XltPropertyNames.ReportGenerator.Errors.STACKTRACES_LIMIT, 500);
+
// event settings
groupEventsByTestCase = getBooleanProperty(PROP_PREFIX + "events.groupByTestCase", true);
eventLimit = getIntProperty(PROP_PREFIX + "events.eventLimit", 100);
@@ -791,7 +795,7 @@ public int getErrorDetailsChartLimit()
/**
* The maximum number of directory hints remembered for a certain error (stack trace).
*
- * @return the maximum number of error traces to list
+ * @return the maximum number of directories to list
*/
public int getDirectoryLimitPerError()
{
@@ -801,12 +805,22 @@ public int getDirectoryLimitPerError()
/**
* The chance to replace directory hints remembered for a certain error (stack trace) when the maximum number is reached.
*
- * @return the chance to replace listed error traces
+ * @return the chance to replace listed directory hints
*/
public double getDirectoryReplacementChance()
{
return directoryReplacementChance;
}
+
+ /**
+ * The maximum number of errors that will be saved complete with their stack trace.
+ *
+ * @return the maximum number of error stack traces displayed in the report
+ */
+ public int getStackTracesLimit()
+ {
+ return stackTracesLimit;
+ }
/**
* If true then the error details charts should be created.
diff --git a/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java b/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java
index 522ef4229..6d3de4858 100644
--- a/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java
+++ b/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java
@@ -95,6 +95,8 @@ public List getDiffElementSpecifications()
specs.add(new ElementSpecification("/testreport/requests/request", "name"));
specs.add(new ElementSpecification("/testreport/pageLoadTimings/pageLoadTiming", "name"));
specs.add(new ElementSpecification("/testreport/customTimers/customTimer", "name"));
+ specs.add(new ElementSpecification("/testreport/customValues/customValue", "name"));
+ specs.add(new ElementSpecification("/testreport/webVitalsList/webVitals", "name"));
specs.add(new ElementSpecification("/testreport/summary/*", "name"));
return specs;
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 988a0cbf3..445d04e27 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java
@@ -158,6 +158,8 @@ public AgentReport createAgentReport()
agentReport.transactions = transactions;
agentReport.transactionErrors = transactionErrors;
+
+ agentReport.transactionErrorPercentage = ReportUtils.calculatePercentage(transactionErrors, transactions);
return agentReport;
}
diff --git a/src/main/java/com/xceptance/xlt/report/providers/AgentReport.java b/src/main/java/com/xceptance/xlt/report/providers/AgentReport.java
index 88b38d20b..c2dc71cc6 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/AgentReport.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/AgentReport.java
@@ -45,4 +45,6 @@ public class AgentReport
public int transactions;
public int transactionErrors;
+
+ public BigDecimal transactionErrorPercentage;
}
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 87514e8d1..2deff1787 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java
@@ -121,7 +121,8 @@ public TimerReport createTimerReport(final boolean generateHistogram)
// set the counts
final double count = runTimeStatistics.getCount();
final long duration = Math.max((getEndTime() - getStartTime()) / 1000, 1);
-
+
+ timerReport.errorPercentage = ReportUtils.calculatePercentage(totalErrors, (int) count);
timerReport.count = (int) count;
timerReport.countPerSecond = ReportUtils.convertToBigDecimal(count / duration);
timerReport.countPerMinute = ReportUtils.convertToBigDecimal(count * 60 / duration);
diff --git a/src/main/java/com/xceptance/xlt/report/providers/ErrorsReportProvider.java b/src/main/java/com/xceptance/xlt/report/providers/ErrorsReportProvider.java
index 86107130d..2e508ff02 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/ErrorsReportProvider.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/ErrorsReportProvider.java
@@ -71,6 +71,11 @@ public class ErrorsReportProvider extends AbstractReportProvider
* which is given in percent.
*/
private double directoryReplacementChance;
+
+ /**
+ * The maximum number of errors that will be saved complete with their stack trace.
+ */
+ private int stackTracesLimit;
/**
* The root directory of the result set.
@@ -137,6 +142,7 @@ public void setConfiguration(final ReportProviderConfiguration config)
// cache some configuration values
directoryLimitPerError = getConfiguration().getDirectoryLimitPerError();
directoryReplacementChance = getConfiguration().getDirectoryReplacementChance();
+ stackTracesLimit = getConfiguration().getStackTracesLimit();
resultsDirectory = getConfiguration().getResultsDirectory();
}
@@ -425,11 +431,19 @@ public void processDataRecord(final Data stat)
if (errorValues == null)
{
final ErrorReport errorReport = new ErrorReport();
- errorReport.trace = trace;
errorReport.message = txnStats.getFailureMessage();
errorReport.testCaseName = testCaseName;
errorReport.actionName = failedActionName;
errorReport.detailChartID = 0;
+
+ if (errorReports.size() < stackTracesLimit) //only save stacktraces up to limit
+ {
+ errorReport.trace = trace;
+ }
+ else
+ {
+ errorReport.trace = "n/a";
+ }
errorValues = new ErrorValues(errorReport);
errorReports.put(key, errorValues);
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 7205ba751..4d90a72bd 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java
@@ -17,6 +17,7 @@
import java.awt.Color;
import java.io.File;
+import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.stream.Collectors;
@@ -264,6 +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[]{};
+ if (countPerSegment != null)
+ {
+ for (int n = 0; n < countPerSegment.getCountPerSegment().length; n++)
+ {
+ timerReport.percentagePerInterval[n] = ReportUtils.calculatePercentage(countPerSegment.getCountPerSegment()[n], timerReport.count);
+ }
+ }
final long duration = Math.max((getConfiguration().getChartEndTime() - getConfiguration().getChartStartTime()) / 1000, 1);
diff --git a/src/main/java/com/xceptance/xlt/report/providers/RequestReport.java b/src/main/java/com/xceptance/xlt/report/providers/RequestReport.java
index d71525509..ed382a089 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/RequestReport.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/RequestReport.java
@@ -15,6 +15,8 @@
*/
package com.xceptance.xlt.report.providers;
+import java.math.BigDecimal;
+
import com.thoughtworks.xstream.annotations.XStreamAlias;
/**
@@ -72,9 +74,15 @@ public class RequestReport extends TimerReport
* The number of timer values per configured runtime interval.
*/
public int[] countPerInterval;
-
+
+ /**
+ * The percentage of timer values per configured runtime interval.
+ */
+ public BigDecimal[] percentagePerInterval;
/**
* A list of the top URLs.
*/
public UrlData urls;
+
+
}
diff --git a/src/main/java/com/xceptance/xlt/report/providers/SummaryReportProvider.java b/src/main/java/com/xceptance/xlt/report/providers/SummaryReportProvider.java
index e640a0703..a6818c2de 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/SummaryReportProvider.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/SummaryReportProvider.java
@@ -91,7 +91,12 @@ else if (data instanceof ActionData)
{
actionDataProcessor.processDataRecord(data);
}
- else if (data instanceof TransactionData || data instanceof EventData)
+ else if (data instanceof TransactionData)
+ {
+ transactionDataProcessor.processDataRecord(data);
+ agentDataProcessor.incrementTransactionCounters(((TransactionData) data).hasFailed());
+ }
+ else if (data instanceof EventData)
{
transactionDataProcessor.processDataRecord(data);
}
diff --git a/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java b/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java
index cea39ef5c..b084cffee 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java
@@ -40,29 +40,34 @@ public class TimerReport
public int count;
/**
- * The number how often the timer has fired.
+ * The calculated number how often the timer has fired per second.
*/
public BigDecimal countPerSecond;
/**
- * The number how often the timer has fired.
+ * The calculated number how often the timer has fired per minute.
*/
public BigDecimal countPerMinute;
/**
- * The number how often the timer has fired.
+ * The calculated number how often the timer has fired per hour.
*/
public BigDecimal countPerHour;
/**
- * The number how often the timer has fired.
+ * The calculated number how often the timer has fired per day.
*/
public BigDecimal countPerDay;
/**
- * The number how often the request has failed.
+ * The number how often the timed item has failed.
*/
public int errors;
+
+ /**
+ * The calculated number which percentage of the total timed items (see count) has failed.
+ */
+ public BigDecimal errorPercentage;
/**
* The minimum timer runtime.
diff --git a/src/main/java/com/xceptance/xlt/report/util/ReportUtils.java b/src/main/java/com/xceptance/xlt/report/util/ReportUtils.java
index 2b3b72a4a..d6cc6e46b 100644
--- a/src/main/java/com/xceptance/xlt/report/util/ReportUtils.java
+++ b/src/main/java/com/xceptance/xlt/report/util/ReportUtils.java
@@ -338,4 +338,24 @@ public static String obtainProjectName(final Iterable documents)
}
return StringUtils.defaultString(projectName);
}
+
+ /**
+ * Calculates the percentage given by count/total, as a {@link BigDecimal} (rounding it to have at most
+ * {@value #DEFAULT_DECIMAL_PLACES} decimal places). Returns 0 if total is 0.
+ *
+ * @param count
+ * @param total
+ * @return
+ */
+ public static BigDecimal calculatePercentage(int count, int total)
+ {
+ if (total == 0)
+ {
+ return new BigDecimal(0);
+ }
+ else
+ {
+ return convertToBigDecimal((double) count * 100 / total);
+ }
+ }
}
diff --git a/src/main/java/org/htmlunit/WebClientOptions.java b/src/main/java/org/htmlunit/WebClientOptions.java
index 7478ef64b..5e25d3e84 100644
--- a/src/main/java/org/htmlunit/WebClientOptions.java
+++ b/src/main/java/org/htmlunit/WebClientOptions.java
@@ -652,7 +652,12 @@ public void setSSLTrustStore(final URL sslTrustStoreUrl, final String sslTrustSt
}
}
+ // GH#497 start
+ /*
void setSSLTrustStore(final KeyStore keyStore) {
+ */
+ public void setSSLTrustStore(final KeyStore keyStore) {
+ // GH#497 end
sslTrustStore_ = keyStore;
}