+
+
+
+
\ No newline at end of file
diff --git a/config/xsl/diffreport/text/descriptions.xsl b/config/xsl/diffreport/text/descriptions.xsl
index 7517711bc..01d341f1b 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.
+
From e331b72ac70a86211e02f9450d19d9ddbd119bcd Mon Sep 17 00:00:00 2001
From: xceptance-dan <122790040+xceptance-dan@users.noreply.github.com>
Date: Tue, 23 Apr 2024 12:46:57 +0200
Subject: [PATCH 04/13] 307 sorting and filtering via hash in online report
feature updated (#365)
* Implemented Sorting And Filtering via Hash
* Moved registerSortAndFilterListeners() -> has to be called always.
* Registering sorting and filter listeners now depends on load event
* Fixed event handling (FF was always faster than Chrome regarding load event)
* Added sorting support for webvitals (load test)
* Implemented id and support for trend reports
* Added ids for for diff report
---
config/testreport/js/xlt.js | 266 +++++++++++++++++-
.../xsl/diffreport/sections/custom-values.xsl | 22 +-
config/xsl/diffreport/sections/web-vitals.xsl | 13 +-
config/xsl/diffreport/util/timer-section.xsl | 32 +--
config/xsl/diffreport/util/timer-table.xsl | 29 +-
config/xsl/loadreport/sections/agents.xsl | 28 +-
.../xsl/loadreport/sections/content-types.xsl | 6 +-
.../xsl/loadreport/sections/custom-values.xsl | 21 +-
config/xsl/loadreport/sections/errors.xsl | 14 +-
config/xsl/loadreport/sections/events.xsl | 8 +-
config/xsl/loadreport/sections/hosts.xsl | 6 +-
.../loadreport/sections/response-codes.xsl | 6 +-
config/xsl/loadreport/sections/web-vitals.xsl | 14 +-
config/xsl/loadreport/util/timer-section.xsl | 84 +++---
config/xsl/loadreport/util/timer-table.xsl | 43 +--
config/xsl/trendreport/util/timer-table.xsl | 4 +
16 files changed, 432 insertions(+), 164 deletions(-)
diff --git a/config/testreport/js/xlt.js b/config/testreport/js/xlt.js
index 15d0391a3..8eef58adf 100644
--- a/config/testreport/js/xlt.js
+++ b/config/testreport/js/xlt.js
@@ -12,17 +12,20 @@
var path = window.location.pathname.split( '/' );
var currentDocument = path[path.length - 1];
- if (targetDocument == currentDocument || targetDocument == "") {
- // before we run it, check that this exists
- if (targetHashText.length > 0) {
- // quote any "." in the hash, otherwise JQuery interprets the following chars as class
- targetHashText = targetHashText.replace(/\./g, "\\.");
- // we scroll with offset to counter our sticky headers
- $.scrollTo(targetHashText, 250, {easing:'swing', offset: {top: -120}});
-
- return false;
+ var hashObj = splitHash(targetHashText);
+ targetHashText = hashObj.navigation;
+
+ if(targetHashText != undefined){
+ if (targetDocument == currentDocument || targetDocument == "") {
+ // before we run it, check that this exists
+ if (targetHashText.length > 0) {
+ // quote any "." in the hash, otherwise JQuery interprets the following chars as class
+ targetHashText = targetHashText.replace(/\./g, "\\.");
+ $.scrollTo(targetHashText, 250, {easing:'swing', offset: {top: -35}});
+ return false;
+ }
}
- }
+ }
}
return true;
@@ -433,6 +436,13 @@
(function setupBackToTopHandler() {
$('nav').click( function(event) {
$.scrollTo(0, 250, {easing:'swing'});
+ // if there is an anchor remove it from the hash
+ if(window.location.hash != '')
+ {
+ var newHashObj = splitHash(window.location.hash);
+ newHashObj.navigation = '';
+ updateHash(newHashObj);
+ }
});
// stop stopPropagation
$('nav li a').click( function(event) {
@@ -455,6 +465,17 @@
if (hiddenContent) {
hiddenContent.classList.remove("hidden");
}
+
+ // We have to wait for the table.js to finish processing before registering events and
+ // may be calling switchToTargetTabIfRequired in sort
+ if (document.readyState === 'complete') {
+ // console.log("load already done");
+ registerSortAndFilterListeners();
+ }
+ else {
+ // console.log("attach event listener");
+ window.addEventListener("load", registerSortAndFilterListeners);
+ }
})();
// the table filters
@@ -655,6 +676,231 @@
// see if we jumped and now have to scroll
scrollTo();
});
+
+ // method that is called when the hash is updated. This happens if the user clicks on local anchors (table, charts), sorts tables by sortable table rows, updates the hash
+ // directly in the URL or if the hash is updated via code
+ function hashChanged(event){
+
+ // in some cases we have create a new hash out of a combination (old + new hash). For example, clicking on a request to get to the request charts totally wipes the hash.
+ // therefore we have to restore the sorting option and filter if there were any provided previously
+ var oldHashObj = splitHash(event.oldURL);
+ var newHashObj = splitHash(event.newURL);
+
+ // hashes might contain a sorting option
+ if(oldHashObj.sort != undefined && newHashObj.sort == undefined){
+ newHashObj.sort = oldHashObj.sort;
+ }
+
+ // hashes might contain a filter
+ if(oldHashObj.filter != undefined && newHashObj.filter == undefined){
+ newHashObj.filter = oldHashObj.filter;
+ }
+
+ // sometimes sorting must be triggered from the update hash function. For example, when a user directly changes the sorting option (navbar) in the url
+ if(newHashObj.sort != undefined){
+ var sortParam = newHashObj.sort.split('=');
+ var sortingElem = document.getElementById(sortParam[0]);
+ var sortingRule = sortParam[1];
+
+ if(sortingElem != null){
+ // this checks if a sorting is required (URL manually edited and not by clicking on a table row)
+ if(sortingElem.classList.contains("table-sorted-" + sortingRule) == false){
+ sort(sortingElem, sortingRule);
+ }
+ }
+ else{
+ alert('No sorting element with given ID found: ' + sortParam[0])
+ }
+ }
+
+ updateHash(newHashObj);
+ }
+
+ // splits the given hash - automatically tries to detect the current format. the returned hash object might contain a "navigation", "sort" and "filter" option
+ function splitHash(hash){
+ var hashObj = {};
+
+ if(hash !== ""){
+ // hash format: http://...#abc
+ var pos = hash.lastIndexOf('#');
+ if (pos >= 0) {
+ hash = hash.slice(pos);
+ // hash format: #abc
+ if(hash.startsWith('#')){
+ hash = hash.split('#')[1];
+ }
+
+ var split = hash.split('&');
+ for(var i = 0; i < split.length; i++){
+ var param = split[i];
+ if(param.startsWith('sort')){
+ hashObj.sort = param;
+ }
+ else if(param.startsWith('filter')){
+ hashObj.filter = param;
+ }
+ else{
+ hashObj.navigation = '#' + param;
+ }
+ }
+ }
+ }
+
+ return hashObj;
+ }
+
+ // updates the URL hash to the parameters passed in updateHashObj. This change is only applied if the hashObj is different from the current hash.
+ // if the update is applied a hashchanged event is fired which then calls "hashChanged"
+ function updateHash(updatedHashObj){
+ var newHash = [];
+
+ // check the possible parameters for the hash
+ if(updatedHashObj.navigation != undefined){
+ newHash.push(updatedHashObj.navigation);
+ }
+
+ if(updatedHashObj.sort != undefined){
+ newHash.push(updatedHashObj.sort);
+ }
+
+ if(updatedHashObj.filter != undefined){
+ newHash.push(updatedHashObj.filter);
+ }
+
+ // check if we have a hash to process
+ if(newHash.length > 0){
+ // create the new hash string -> filter out empty elements (required for removal of anchors in the hash)
+ var newJoinedHash = newHash.filter(n => n).join('&');
+ if(newJoinedHash.startsWith('#') == false){
+ newJoinedHash = '#' + newJoinedHash;
+ }
+
+ // check if the hash is different: only then update it
+ if(window.location.hash != newJoinedHash){
+ // updated hash to params
+ window.location.hash = newJoinedHash;
+ }
+ }
+ }
+
+ // eventlistener that fires if a sortable table row gets clicked
+ function updateHashAfterSort(sortingEvent){
+ if(sortingEvent.target.classList.contains('table-sortable') && sortingEvent.target.id != undefined){
+ var sortingRule = sortingEvent.target.classList.contains('table-sorted-asc') ? 'asc' : 'desc';
+ // Get the current hash (if one exists)
+ var hashObj = splitHash(window.location.hash);
+
+ // update the sorting option of the hash
+ hashObj.sort = sortingEvent.target.id + '=' + sortingRule;
+
+ // trigger the hash change
+ updateHash(hashObj);
+ }
+ }
+
+ // switches to the given tab if the current one is different
+ function switchToTargetTabIfRequired(targetTab){
+ var requestPageActiveTab = document.querySelector('#tabletabbies > .c-tab[id].c-is-active');
+ if(requestPageActiveTab != null){
+ var currentTabId = requestPageActiveTab.getAttribute('id');
+
+ var targetTabId = targetTab.getAttribute('id');
+
+ // if the current tab is different from the target tab containing the sorting option switch tabs
+ if(currentTabId != targetTabId){
+ document.querySelector('#tabletabbies .c-tabs-nav-link a[href=\'#' + targetTabId + '\']').click()
+ }
+ }
+ }
+
+ // sorts the passed table row by the given rule -> either ascending (asc) or descending (desc). Invalid options or elements trigger an alert and are ignored
+ function sort(elem, rule) {
+ if(elem != null){
+ if(rule == 'asc' || rule == 'desc'){
+ // if users are on the request page we need to check in which tab the target sorting option is located
+ var targetTab = elem.closest('.c-tab[id]');
+ switchToTargetTabIfRequired(targetTab);
+
+ var classList = elem.classList;
+ // only sort if the sorting rule is not already applied on the element
+ if(classList.contains("table-sorted-" + rule) == false){
+ while(elem.classList.contains("table-sorted-" + rule) == false){
+ // Click sorting
+ elem.click();
+ }
+ }
+ }
+ else{
+ alert('Sorting only suppports \'asc\' or \'desc\' as parameter')
+ }
+ }
+ else{
+ alert('Target element for sorting does not exist');
+ }
+ }
+
+ // method that gets triggered when the user enters some input to apply a filter
+ function updateHashAfterFilter(filterEvent){
+ var filter = filterEvent.target.value;
+ var encodedFilter = encodeURIComponent(filterEvent.target.value);
+ // console.log('filter change:' + filter + ' to ' + encodedFilter);
+
+ var newHashObj = splitHash(window.location.hash);
+ newHashObj.filter = 'filter=' + encodedFilter;
+
+ updateHash(newHashObj);
+ }
+
+ // $(window).on( "load", function(){
+ function registerSortAndFilterListeners(){
+ // Prepare Hash Monitoring
+
+ // once everything is loaded check whether there is a sorting rule passed
+ var hashObj = splitHash(window.location.hash);
+ if(hashObj.sort != undefined){
+ // Perform initial sorting
+ var sortParam = hashObj.sort.split('=');
+ var sortingElem = document.getElementById(sortParam[0]);
+ var sortingRule = sortParam[1];
+ sort(sortingElem, sortingRule);
+ }
+
+ // check for existing filter to apply
+ if(hashObj.filter != undefined){
+ // Apply initial filter
+ var filterParam = hashObj.filter.split('=');
+ var encodedFilter = filterParam[1];
+
+ if(encodedFilter.length > 0){
+ var decodedFilter = decodeURIComponent(encodedFilter);
+ // console.log('filter value: ' + decodedFilter);
+ var filterInputFields = document.querySelectorAll('input.filter');
+ for(var i = 0; i < filterInputFields.length; i++){
+ var filterField = filterInputFields[i];
+ filterField.value = decodedFilter;
+ filter(filterField);
+ }
+ }
+ }
+
+ // Register sorting listeners
+ var sortableTableRows = document.getElementsByClassName('table-sortable');
+ for(var i = 0; i < sortableTableRows.length; i++){
+ sortableTableRows[i].addEventListener('click', updateHashAfterSort);
+ }
+
+ // Register filter listeners
+ var filterInputFields = document.querySelectorAll('input.filter');
+ for(var i = 0; i < filterInputFields.length; i++){
+ // filterInputFields[i].addEventListener('input', updateHashAfterFilter);
+ filterInputFields[i].addEventListener('focusout', updateHashAfterFilter);
+ }
+
+ // Listeners applied, hook on the hashchange event
+ window.addEventListener('hashchange', hashChanged);
+ //});
+ }
+
})(jQuery)
/*
diff --git a/config/xsl/diffreport/sections/custom-values.xsl b/config/xsl/diffreport/sections/custom-values.xsl
index 3803b63d8..925e7fe97 100644
--- a/config/xsl/diffreport/sections/custom-values.xsl
+++ b/config/xsl/diffreport/sections/custom-values.xsl
@@ -15,6 +15,7 @@
#ReportInfo-
+ sortByRuntable-sortable:numeric cluetip
From b0ba8e5b81f9ab42a3bd519457ebfccdfcca814a Mon Sep 17 00:00:00 2001
From: Julia <51991513+js-xc@users.noreply.github.com>
Date: Wed, 24 Apr 2024 12:02:37 +0200
Subject: [PATCH 05/13] #439: Limit the number of error stack traces in memory
during report generation
---
config/reportgenerator.properties | 5 +++++
.../xceptance/xlt/common/XltPropertyNames.java | 2 ++
.../report/ReportGeneratorConfiguration.java | 18 ++++++++++++++++--
.../report/providers/ErrorsReportProvider.java | 16 +++++++++++++++-
4 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/config/reportgenerator.properties b/config/reportgenerator.properties
index 675cf556d..d642621db 100644
--- a/config/reportgenerator.properties
+++ b/config/reportgenerator.properties
@@ -65,6 +65,11 @@ com.xceptance.xlt.reportgenerator.requests.removeIndexes = true
## across both agents and load test duration in the report.
#com.xceptance.xlt.reportgenerator.errors.directoryReplacementChance = 0.1
+## The maximum number of errors for which stack traces will be displayed (500 by
+## default). Setting a lower limit can prevent the report generator from running
+## out of memory if there are many different errors.
+#com.xceptance.xlt.reportgenerator.errors.stackTracesLimit = 500
+
## Width and height of the generated charts (in pixels).
com.xceptance.xlt.reportgenerator.charts.width = 900
com.xceptance.xlt.reportgenerator.charts.height = 300
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/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/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);
From 045f9f2f037ba4888a545714600ae47c55f512e8 Mon Sep 17 00:00:00 2001
From: Julia <51991513+js-xc@users.noreply.github.com>
Date: Wed, 24 Apr 2024 13:11:32 +0200
Subject: [PATCH 06/13] #474: Output error percentage values as part of the XML
data output
* error percentages in xml
* put runtime segmentation percentages in xml
* percentage for single agents in report
* put error percentage for agent summary in xml
* adjusted templates to changed xml structure
---
config/xsl/loadreport/agents.xsl | 1 +
config/xsl/loadreport/sections/agents.xsl | 31 +++++--------------
.../xsl/loadreport/util/summary-timer-row.xsl | 8 +----
config/xsl/loadreport/util/timer-row.xsl | 18 ++---------
.../xsl/loadreport/util/timer-summary-row.xsl | 19 +++---------
.../report/providers/AgentDataProcessor.java | 2 ++
.../xlt/report/providers/AgentReport.java | 2 ++
.../providers/BasicTimerDataProcessor.java | 3 +-
.../providers/RequestDataProcessor.java | 9 ++++++
.../xlt/report/providers/RequestReport.java | 10 +++++-
.../providers/SummaryReportProvider.java | 7 ++++-
.../xlt/report/providers/TimerReport.java | 15 ++++++---
.../xlt/report/util/ReportUtils.java | 20 ++++++++++++
13 files changed, 76 insertions(+), 69 deletions(-)
diff --git a/config/xsl/loadreport/agents.xsl b/config/xsl/loadreport/agents.xsl
index 557fe8ba6..0c5a371f1 100644
--- a/config/xsl/loadreport/agents.xsl
+++ b/config/xsl/loadreport/agents.xsl
@@ -61,6 +61,7 @@
-->
+
diff --git a/config/xsl/loadreport/sections/agents.xsl b/config/xsl/loadreport/sections/agents.xsl
index f1ed59990..b5ac57f8e 100644
--- a/config/xsl/loadreport/sections/agents.xsl
+++ b/config/xsl/loadreport/sections/agents.xsl
@@ -3,6 +3,7 @@
+
@@ -53,12 +54,6 @@
-
-
-
-
-
-
@@ -66,25 +61,19 @@
-
+
-
+ value number error
-
+
-
+ value number error
-
-
-
-
-
-
-
+ %
@@ -163,13 +152,7 @@
value number error
-
-
-
-
-
-
-
+ %
-
-
-
-
-
-
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/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);
+ }
+ }
}
From 88018c857110149d8bedca40494e413ab8bf4248 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 24 Apr 2024 13:23:57 +0200
Subject: [PATCH 07/13] Bump org.apache.commons:commons-configuration2 from
2.8.0 to 2.10.1 (#485)
Bumps org.apache.commons:commons-configuration2 from 2.8.0 to 2.10.1.
---
updated-dependencies:
- dependency-name: org.apache.commons:commons-configuration2
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index b2f1e171b..0480c20c2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -259,7 +259,7 @@
org.apache.commonscommons-configuration2
- 2.8.0
+ 2.10.1org.apache.commons
From fdca07697373530fca8ce977af11528262215a6f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 24 Apr 2024 13:31:24 +0200
Subject: [PATCH 08/13] Bump follow-redirects from 1.15.5 to 1.15.6 in
/resultbrowser (#481)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)
---
updated-dependencies:
- dependency-name: follow-redirects
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
resultbrowser/package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/resultbrowser/package-lock.json b/resultbrowser/package-lock.json
index 3e57f0a0a..4849c1ae3 100644
--- a/resultbrowser/package-lock.json
+++ b/resultbrowser/package-lock.json
@@ -2152,9 +2152,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
From 079160b362114d3e07fa977cdb729a9609675579 Mon Sep 17 00:00:00 2001
From: Rene Schwietzke
Date: Thu, 25 Apr 2024 16:46:37 +0200
Subject: [PATCH 09/13] Update README.md
Added XTC information
---
README.md | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b8813605f..f8642f1da 100644
--- a/README.md
+++ b/README.md
@@ -62,5 +62,10 @@ When you build XLT by yourself, the following limitations apply.
* Depending on the distribution you use, you might need to adjust the path to your Chrome/Chromium executable in `build.properties` on Linux as well.
+# XLT as a Service - XTC
+If you are looking for a hosted version of XLT with all the bells and whistles, look no further. Xceptance offers XTC - The Xceptance Test Center, a fully hosted, multi-project, infinitely scalable version of XLT in the cloud. [Contact](https://www.xceptance.com/en/contact/) us for more details.
+
+![image](https://github.com/Xceptance/XLT/assets/1793856/7a8efb7b-caef-4aaf-9321-cdcaba673d8a)
+
# Jobs
-Do you like the code and architecture of XLT? Do you have ideas for new features or just like to work with it? Why not considering to apply for a job at Xceptance? We are always hiring developers and testers. Just drop us a line. You can find more information on our career page [Jobs at Xceptance](https://www.xceptance.com/en/careers/).
+Do you like XLT's code and architecture? Do you have ideas for new features or just enjoy working with it? Consider applying for a job at Xceptance. We are always hiring developers and testers. Just drop us a line. You can find more information on our career page [Jobs at Xceptance] (https://www.xceptance.com/en/careers/).
From f67b8332b2422f5765bcb4675f37acdb08843706 Mon Sep 17 00:00:00 2001
From: Rene Schwietzke
Date: Thu, 25 Apr 2024 16:47:59 +0200
Subject: [PATCH 10/13] Update README.md
Docs link updated, forum removed
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f8642f1da..7fd26d808 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# XLT
-XLT is an extensive load and performance test tool developed and maintained by Xceptance. If you need more information, here is the current [website](https://www.xceptance.com/xlt/) and the current [documentation portal](https://xltdoc.xceptance.com/). There is also a [forum](https://ask.xceptance.de/) available to discuss load testing and test automation with XLT.
+XLT is an extensive load and performance test tool developed and maintained by Xceptance. If you need more information, here is the current [website](https://www.xceptance.com/xlt/) and the current [documentation portal](https://docs.xceptance.com/).
The documentation was fully rewritten in 2020 and it is continuously updated. We appreciate your feedback. You can also directly contribute on GitHub at https://github.com/Xceptance/xlt-documentation. If you need any information from the legacy documentation, you can still find it at https://lab.xceptance.de/releases/xlt/5.7.1/index.html
From f4516769fe8df06c84e155818a531bbd6f05572c Mon Sep 17 00:00:00 2001
From: Julia <51991513+js-xc@users.noreply.github.com>
Date: Tue, 28 May 2024 14:13:45 +0200
Subject: [PATCH 11/13] #465: Responsive Result Browser
* bigger font in menu
* column layout in mobile/portrait mode
* filter menu made usable on mobile (full screen overlay)
* navigation can be expanded/hidden on mobile in portrait mode
* realbrowser action screenshots are sized to 100% screen width per default, zoomable
---
resultbrowser/src/css/default.css | 252 +++++++++++++++++++++++++-
resultbrowser/src/index.html | 20 +-
resultbrowser/src/js/resultbrowser.js | 133 +++++++++++++-
3 files changed, 385 insertions(+), 20 deletions(-)
diff --git a/resultbrowser/src/css/default.css b/resultbrowser/src/css/default.css
index f13dcb257..a4a88ac35 100644
--- a/resultbrowser/src/css/default.css
+++ b/resultbrowser/src/css/default.css
@@ -23,7 +23,7 @@ body {
* The header
*/
#header {
- background-color: #A50000;
+ background-color: #A50000;
}
#header .logo {
@@ -40,6 +40,7 @@ body {
#header .logo h1 {
font-size: 16px;
color: white;
+ align-content: center;
}
/*
@@ -64,8 +65,12 @@ body {
#menu-icon {
color: white;
- margin-left: 8px;
+ margin: 5px;
+ margin-left: auto;
+ font-size: 20px;
+ font-weight: bold;
cursor: pointer;
+ transform: rotate(90deg);
}
#menu {
@@ -211,7 +216,7 @@ body {
cursor: pointer;
}
-#navigation .expander::after {
+#navigation .expander::before {
font-style: normal;
content: "◢";
margin-right: 0.5em;
@@ -220,7 +225,7 @@ body {
transform: translateY(-10%) rotate(-45deg);
}
-#navigation .expander.expanded::after {
+#navigation .expander.expanded::before {
transform: translateY(-10%) rotate(0deg);
}
@@ -627,3 +632,242 @@ table.key-value-table td.value .csep {
transform: rotate(360deg);
}
}
+
+.actionimgScale {
+ position: absolute;
+ z-index: 10;
+ width: 50px;
+ height: 50px;
+ background: rgba(255, 255, 255, 0.5);
+ align-content: center;
+ text-align: center;
+ border-radius: 5px;
+ border: 1px solid #bbb;
+ cursor: pointer;
+}
+
+.actionimgScale#actionimgSwitch{
+ right: 20px;
+ bottom: 20px;
+}
+
+.actionimgScale#actionimgSwitch.full {
+ font-size: 30px;
+}
+
+.actionimgScale#actionimgSwitch.fit {
+ font-size: 20px;
+}
+
+/********************************************************************************************
+ *
+ * Responsive
+ *
+ ********************************************************************************************/
+
+#wrapper, #mainContent {
+ flex-flow: row;
+ width: 100vw !important;
+}
+
+#header .expander {
+ display: none;
+}
+
+#menu-close {
+ display: none;
+}
+
+/*responsive behavior for phones in portrait mode*/
+@media (max-width: 599px) {
+
+ #header {
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 2;
+ }
+
+ #header .logo h1 {
+ font-size: 22px;
+ }
+
+ #wrapper, #mainContent {
+ flex-flow:column;
+ }
+
+ #header .expander {
+ cursor: pointer;
+ }
+
+ #header .expander {
+ font-weight: bold;
+ font-size: 22px;
+ color: #fff;
+ display: inline-block;
+ text-align: center;
+ align-content: center;
+ width: 35px;
+ transform: rotate(0deg);
+ transition: transform .2s;
+
+ }
+
+ #header .expander.expanded {
+ transform: rotate(90deg);
+ }
+
+ #menu-icon {
+ font-size: 24px;
+ }
+
+ #leftSideMenu {
+ height: 90px;
+ overflow: hidden;
+ transition: height .2s;
+ }
+
+ #leftSideMenu.expanded {
+ height: 80vh;
+ overflow: scroll;
+ }
+
+ #navigation {
+ padding-top: 50px;
+ }
+
+ #content {
+ height: 20vh;
+ transition: height .2s;
+ border-top: 1px solid #bbb;
+ }
+
+ #content.expanded {
+ height: 100vh;
+ }
+
+ #transaction {
+ overflow: visible;
+ font-size: 19px;
+ }
+
+ #actionlist {
+ flex-basis: auto !important;
+ margin: 0px;
+ }
+
+ #menu > ul {
+ flex-flow: column;
+ }
+}
+
+/*responsive behavior for phones in portrait and landscape mode*/
+@media (max-width: 599px), (max-height: 400px) {
+
+ #header .logo h1 {
+ align-content: center;
+ margin: 0px;
+ }
+
+ #navigation {
+ font-size: 16px;
+ }
+
+ #navigation li.action {
+ padding-top: 6px;
+ padding-bottom: 6px;
+ }
+
+ #navigation ul li ul li {
+ font-size: 16px;
+ padding: 4px;
+ }
+
+ #navigation .expander {
+ margin: 0px 5px;
+ }
+
+ #responseContentActions {
+ position: fixed;
+ bottom: 10px;
+ right: 10px;
+ top: auto;
+ }
+
+ #beautify, #highlightSyntax, #selectResponseContent {
+ background-color: #666;
+ border: none;
+ border-radius: 13px;
+ color: #fff;
+ padding: 8px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ cursor: pointer;
+ }
+
+ #transactionContent {
+ border-top: 2px solid #666;
+ }
+
+ #transactionContent h1 {
+ margin: 8px 2px;
+ font-size: 12pt;
+ }
+
+ .actionimgScale#actionimgSwitch {
+ right: 10px;
+ bottom: 10px;
+ }
+
+ .tabs .tabs-nav {
+ padding: 4px;
+ }
+
+ .tabs .tabs-nav li {
+ border-radius: 4px;
+ background-color: #db0009;
+ border: 0px;
+ margin: 2px;
+ }
+
+ .tabs .tabs-nav li.selected {
+ border: 0px;
+ }
+
+ #menu {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background-color: #fffd;
+ border: 0px;
+ }
+
+ #menu > ul {
+ display: flex;
+ align-items: stretch;
+ }
+
+ #menu li:nth-of-type(2n) .menu-group {
+ border-left: 0px;
+ border-right: 0px;
+ }
+
+ #menu > ul > li {
+ flex-grow: 1;
+ }
+
+ .menu-group {
+ width: -webkit-fill-available;
+ }
+
+ .open #menu-close {
+ display: block;
+ position: absolute;
+ top: 5px;
+ right: 10px;
+ font-size: 20px;
+ }
+}
+
\ No newline at end of file
diff --git a/resultbrowser/src/index.html b/resultbrowser/src/index.html
index 90e89bb5a..5bfd54388 100644
--- a/resultbrowser/src/index.html
+++ b/resultbrowser/src/index.html
@@ -1,8 +1,9 @@
-
+
+
XLT Result Browser
@@ -24,16 +25,17 @@
-
+
-
+
-
Result Browser ☰
+
Result Browser
-
+ …
+
@@ -49,7 +51,12 @@
Result Browser ☰
-
+
+
+
+
+
⌞⌝
+
@@ -342,6 +349,7 @@
Stored Test Parameters and Result Data
+ X
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"));
From 8c2b3ad0a1cb3b9b65c9e99b422ccaf436614a11 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rg=20Werner?=
<4639399+jowerner@users.noreply.github.com>
Date: Fri, 31 May 2024 17:38:42 +0200
Subject: [PATCH 12/13] #497: Make mTLS more convenient to use (#498)
* initial
* - make key/trust store configurable
- load key/trust stores on first use only and cache them for subsequent uses
* marked our changes in WebClientOptions.java
* added needed Jetty server properties for testing mTLS (inactive by default)
---
samples/app-server/start.ini | 19 +-
.../config/default.properties | 17 ++
.../config/default.properties | 17 ++
.../config/default.properties | 17 ++
.../config/default.properties | 17 ++
.../testsuite-xlt/config/default.properties | 17 ++
.../xceptance/xlt/engine/XltWebClient.java | 202 ++++++++++++++----
.../java/org/htmlunit/WebClientOptions.java | 5 +
8 files changed, 261 insertions(+), 50 deletions(-)
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/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/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;
}
From 3ba483fa78f0565fe35b6d117f282911148df2c6 Mon Sep 17 00:00:00 2001
From: Joerg Werner <4639399+jowerner@users.noreply.github.com>
Date: Mon, 3 Jun 2024 11:23:29 +0200
Subject: [PATCH 13/13] prepared release 8.2.0
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 0480c20c2..4725abcc6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.xceptancexlt
- 8.1.0
+ 8.2.0jarXLT