diff --git a/README.md b/README.md
index 2f0eb04..7a554d3 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,178 @@
-# JUnitReportKpiJMeterReportCsv
+# Generating JUnit Report based on custom Key Performance Indicators (KPIs) applied to the JMeter Report CSV file
+
This tool read KPI declarations in a file and apply the KPI assertion on a JMeter Report CSV file and generates a result file in JUnit XML format.
+
+JMeter Report CSV file is created with Listener :
+- Summary Report, documentation at [Summary Report](https://jmeter.apache.org/usermanual/component_reference.html#Summary_Report)
+- Aggregate Report, documentation at [Aggregate Report](https://jmeter.apache.org/usermanual/component_reference.html#Aggregate_Report)
+- Synthesis Report, documentation at [Synthesis Report](https://jmeter-plugins.org/wiki/SynthesisReport/)
+
+and "Save Table Data" button
+![save_table_data](doc/images/summary_report_save_table_data.png)
+
+JMeter Report CSV could be generated in Command Line Interface (CLI) with :
+- JMeterPluginsCMD tool, documentation at [JMeterPluginsCMD](https://jmeter-plugins.org/wiki/JMeterPluginsCMD/)
+ - E.g : JMeterPluginsCMD.bat --generate-csv aggregate.csv --input-jtl results.csv --plugin-type AggregateReport
+ - JMeterPluginsCMD.bat --generate-csv synthesis.csv --input-jtl results.csv --plugin-type SynthesisReport
+- jmeter-graph-tool-maven-plugin maven plugin, documentation at [jmeter-graph-tool-maven-plugin](https://github.com/vdaburon/jmeter-graph-tool-maven-plugin)
+
+Example of a JMeter Report CSV file (Synthesis Report)
+![jmeter report csv](doc/images/example_csv_file.png)
+
+The first line contains **header** column name
+![headers jmeter report csv](doc/images/headers_jmeter_report_csv_file.png)
+
+## The KPI file format
+The KPI file need 5 columns :
+1) name_kpi the name of the KPI also the classname in the Test Case in JUnit
+2) metric_csv_column_name the column name **header** in the JMeter Report CSV file (**header** likes : `# Samples` or `Average` or `Min` or `Max` or `90% Line` or `Std. Dev.` or `Error %` or `Throughput` or `Received KB/sec` or `Avg. Bytes` or `MY_COLUMN_NAME`)
+3) label_regex the Label name in regular expression, label header in the JMeter Report CSV file (E.g : `SC01_P.*` or `SC\d+_P.*` or `SC01_P01_LOGIN` or `SC01_P01_LOGIN|SC01_P02_HOME` or `\d+ /.*` )
+4) comparator the comparator `<` or `<=` or `>` or `>=`
+5) threshold the value (for percentage rate use value between 0 and 1, e.g : 0.02 for 2%)
+
+The column separator is ',' for the kpi file
+
+name_kpi,metric_csv_column_name,label_regex,comparator,threshold
+Percentiles_90,90% Line,SC\d+_P.*,<=,3000
+Percentiles_90 specific pages,90% Line,SC01_P01_LOGIN|SC01_P02_HOME,<=,4000
+Average Pages,Average,SC\d+_P.*,<=,2000
+Errors rate,Error %,SC\d+_SCRIPT.*,<,0.01
+Page Size,Avg. Bytes,SC.*,<=,512000
+Max time specific API,Max,"010 /api/user/.+",<=,5000
+Number pages,# Samples,SC.*,>,10
+
+
+KPI View in Excel
+![kpi in Excel](doc/images/kpi_excel.png)
+
+Save in UTF-8 comma separator **no BOM** or csv with comma separator if you have only ASCII characters (no accent é,è, à ...)
+
+## Parameters
+The tool have parameters :
+
+usage: io.github.vdaburon.jmeter.utils.reportkpi.JUnitReportFromJMReportCsv -csvJMReport <csvJMReport> [-csvLabelColumnName <csvLabelColumnName>]
+ [-exitReturnOnFail <exitReturnOnFail>] [-help] [-junitFile <junitFile>] -kpiFile <kpiFile>
+io.github.vdaburon.jmeter.utils.reportkpi.JUnitReportFromJMReportCsv
+ -csvJMReport <csvJMReport> JMeter report csv file (E.g : summary.csv)
+ -csvLabelColumnName <csvLabelColumnName> Label Column Name in CSV JMeter Report (Default : Label)
+ -exitReturnOnFail <exitReturnOnFail> if true then when kpi fail then create JUnit XML file and program return exit 1 (KO); if false
+ [Default] then create JUnit XML File and exit 0 (OK)
+ -help Help and show parameters
+ -junitFile <junitFile> junit file name out (Default : jmeter-junit-plugin-jmreport.xml)
+ -kpiFile <kpiFile> KPI file contains rule to check (E.g : kpi.csv)
+E.g : java -jar junit-reporter-kpi-from-jmeter-report-csv-<version>-jar-with-dependencies.jar -csvJMReport summary.csv -kpiFile kpi.csv -exitReturnOnFail true
+or more parameters : java -jar junit-reporter-kpi-from-jmeter-report-csv-<version>-jar-with-dependencies.jar -csvJMReport AggregateReport.csv -csvLabelColumnName Label
+-kpiFile kpi_check.csv -junitFile junit.xml -exitReturnOnFail true
+
+
+## JUnit Report XML file generated
+Example JUnit XML file generated :
+```xml
+
+
+
+ Actual value 63.0 exceeds or equals threshold 30.0 for samples matching "SC\d+_P.*"; fail label(s) "SC01_P01_HOME", "SC03_P01_HOME", "SC03_P03_LOGIN", "SC01_P03_LOGIN", "SC03_P04_LINK_STATS", "SC01_P05_LAUNCH_FIND"
+
+
+ Actual value 79.0 exceeds or equals threshold 60.0 for samples matching "SC01_P05_LAUNCH_FIND"; fail label(s) "SC01_P05_LAUNCH_FIND"
+
+
+
+```
+Remark : failure message is limited to 1024 characters, if failure message finished with "..." then the message is voluntarily truncated.
+
+## JUnit Report in a Gitlab Pipeline
+A JUnit Report with KPIs display in a Gitlab Pipeline
+![junit gitlab pipeline](doc/images/junit_report_in_gitlab_pipeline.png)
+
+If you click on button "View Details" for Status Fail, you will show the fail message
+![junit gitlab pipeline detail fail](doc/images/junit_report_in_gitlab_pipeline_detail_fail.png)
+
+## JUnit Report in Jenkins Build
+A JUnit Report with KPIs display in Jenkins Build
+![junit jenkins build](doc/images/junit_report_jenkins.png)
+
+If you click on link "Name Test" fail , you will show the fail message
+![junit jenkins build detail fail](doc/images/junit_report_jenkins_detail_fail.png)
+
+## License
+See the LICENSE file Apache 2 [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)
+
+## Usage Maven
+The maven groupId, artifactId and version, this plugin is in the **Maven Central Repository** [![Maven Central junit-reporter-kpi-from-jmeter-report-csv](https://maven-badges.herokuapp.com/maven-central/io.github.vdaburon/junit-reporter-kpi-from-jmeter-report-csv/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.vdaburon/junit-reporter-kpi-from-jmeter-report-csv)
+
+```xml
+io.github.vdaburon
+junit-reporter-kpi-from-jmeter-report-csv
+1.1
+```
+Just include the plugin in your `pom.xml` and execute `mvn verify`
+or individual launch `mvn -DjmeterReportFile=synthesis.csv -DkpiFile=kpi.csv -DjunitFile=jmeter-junit-plugin-jmreport.xml exec:java@junit-reporter-kpi-from-jmeter-report-csv`
+
+```xml
+
+
+
+ synthesis.csv
+ kpi.csv
+ jmeter-junit-plugin-jmreport.xml
+
+
+
+
+ io.github.vdaburon
+ junit-reporter-kpi-from-jmeter-report-csv
+ 1.1
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.2.1
+
+
+ create_junit-report-kpi-from-jmeter-report
+ verify
+
+ java
+
+
+ io.github.vdaburon.jmeter.utils.reportkpi.JUnitReportFromJMReportCsv
+
+ -kpiFile
+ ${project.build.directory}/jmeter/testFiles/${kpiFile}
+ -csvJMReport
+ ${project.build.directory}/jmeter/results/${jmeterReportFile}
+ -junitFile
+ ${project.build.directory}/jmeter/results/${junitFile}
+ -exitReturnOnFail
+ true
+
+
+
+
+
+
+
+
+```
+
+## Simple jar tool
+This tool is a java jar, so it's could be use as simple jar (look at [Release](https://github.com/vdaburon/JUnitReportKpiJMeterReportCsv/releases) to download jar file)
+
+java -jar junit-reporter-kpi-from-jmeter-report-csv-<version>-jar-with-dependencies.jar -csvJMReport summary.csv -kpiFile kpi.csv -junitFile junit-report.xml -exitReturnOnFail true
+
+
+## Link to other project
+Usually this plugin is use with [jmeter-graph-tool-maven-plugin](https://github.com/vdaburon/jmeter-graph-tool-maven-plugin)
+
+## Versions
+Version 1.2 change package name (add reportkpi)
+
+Version 1.1 change groupId
+
+Version 1.0 initial version
+
diff --git a/doc/images/example_csv_file.png b/doc/images/example_csv_file.png
new file mode 100644
index 0000000..adf4f18
Binary files /dev/null and b/doc/images/example_csv_file.png differ
diff --git a/doc/images/headers_jmeter_report_csv_file.png b/doc/images/headers_jmeter_report_csv_file.png
new file mode 100644
index 0000000..bec0dac
Binary files /dev/null and b/doc/images/headers_jmeter_report_csv_file.png differ
diff --git a/doc/images/junit_report_in_gitlab_pipeline.png b/doc/images/junit_report_in_gitlab_pipeline.png
new file mode 100644
index 0000000..07f1fa0
Binary files /dev/null and b/doc/images/junit_report_in_gitlab_pipeline.png differ
diff --git a/doc/images/junit_report_in_gitlab_pipeline_detail_fail.png b/doc/images/junit_report_in_gitlab_pipeline_detail_fail.png
new file mode 100644
index 0000000..2a08fed
Binary files /dev/null and b/doc/images/junit_report_in_gitlab_pipeline_detail_fail.png differ
diff --git a/doc/images/junit_report_jenkins.png b/doc/images/junit_report_jenkins.png
new file mode 100644
index 0000000..2784dee
Binary files /dev/null and b/doc/images/junit_report_jenkins.png differ
diff --git a/doc/images/junit_report_jenkins_detail_fail.png b/doc/images/junit_report_jenkins_detail_fail.png
new file mode 100644
index 0000000..a2d1e72
Binary files /dev/null and b/doc/images/junit_report_jenkins_detail_fail.png differ
diff --git a/doc/images/kpi_excel.png b/doc/images/kpi_excel.png
new file mode 100644
index 0000000..dba6e39
Binary files /dev/null and b/doc/images/kpi_excel.png differ
diff --git a/doc/images/summary_report_save_table_data.png b/doc/images/summary_report_save_table_data.png
new file mode 100644
index 0000000..3f93a06
Binary files /dev/null and b/doc/images/summary_report_save_table_data.png differ
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..826dff2
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,170 @@
+
+
+ 4.0.0
+
+ io.github.vdaburon
+ junit-reporter-kpi-from-jmeter-report-csv
+ 1.2
+ jar
+ Create a JUnit XML file with KPI rules from JMeter CSV Report
+ A tool that creates a JUnit XML file with KPI rules from JMeter CSV Report
+ https://github.com/vdaburon/JUnitReportKpiJMeterReportCsv
+ 2023
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+
+ vdaburon
+ Vincent DABURON
+
+ Committer
+
+
+
+
+
+
+
+
+ jmeter-plugins-google-group
+ https://groups.google.com/g/jmeter-plugins
+
+
+
+ https://github.com/vdaburon/JUnitReportKpiJMeterReportCsv.git
+ https://github.com/vdaburon/JUnitReportKpiJMeterReportCsv.git
+ https://github.com/vdaburon/JUnitReportKpiJMeterReportCsv
+ HEAD
+
+
+
+
+ ossrh
+ https://s01.oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+ 1.8
+ 1.8
+ UTF-8
+
+
+
+
+ org.apache.commons
+ commons-csv
+ 1.10.0
+
+
+ commons-cli
+ commons-cli
+ 1.5.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.1
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ ${project.build.sourceEncoding}
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 2.9.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.5.0
+
+
+ package
+
+ single
+
+
+
+
+
+ io.github.vdaburon.jmeter.utils.reportkpi.JUnitReportFromJMReportCsv
+
+
+
+
+ jar-with-dependencies
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.0.1
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.13
+ true
+
+ ossrh
+ https://s01.oss.sonatype.org/
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/CheckKpiResult.java b/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/CheckKpiResult.java
new file mode 100644
index 0000000..ecc03cb
--- /dev/null
+++ b/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/CheckKpiResult.java
@@ -0,0 +1,81 @@
+package io.github.vdaburon.jmeter.utils.reportkpi;
+
+public class CheckKpiResult {
+ private String nameKpi;
+ private String metricCsvColumnName;
+ private String labelRegex;
+ private String comparator;
+ private String threshold;
+ private boolean isKpiFail;
+ private String failMessage;
+
+ public String getNameKpi() {
+ return nameKpi;
+ }
+
+ public void setNameKpi(String nameKpi) {
+ this.nameKpi = nameKpi;
+ }
+
+ public String getMetricCsvColumnName() {
+ return metricCsvColumnName;
+ }
+
+ public void setMetricCsvColumnName(String metricCsvColumnName) {
+ this.metricCsvColumnName = metricCsvColumnName;
+ }
+
+ public String getLabelRegex() {
+ return labelRegex;
+ }
+
+ public void setLabelRegex(String labelRegex) {
+ this.labelRegex = labelRegex;
+ }
+
+ public String getComparator() {
+ return comparator;
+ }
+
+ public void setComparator(String comparator) {
+ this.comparator = comparator;
+ }
+
+ public String getThreshold() {
+ return threshold;
+ }
+
+ public void setThreshold(String threshold) {
+ this.threshold = threshold;
+ }
+
+ public boolean isKpiFail() {
+ return isKpiFail;
+ }
+
+ public void setKpiFail(boolean kpiFail) {
+ isKpiFail = kpiFail;
+ }
+
+ public String getFailMessage() {
+ return failMessage;
+ }
+
+ public void setFailMessage(String failMessage) {
+ this.failMessage = failMessage;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("CheckKpiResult{");
+ sb.append("nameKpi='").append(nameKpi).append('\'');
+ sb.append(", metricCsvColumnName='").append(metricCsvColumnName).append('\'');
+ sb.append(", labelRegex='").append(labelRegex).append('\'');
+ sb.append(", comparator='").append(comparator).append('\'');
+ sb.append(", threshold='").append(threshold).append('\'');
+ sb.append(", isKpiFail=").append(isKpiFail);
+ sb.append(", failMessage='").append(failMessage).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/JUnitReportFromJMReportCsv.java b/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/JUnitReportFromJMReportCsv.java
new file mode 100644
index 0000000..06fe9b7
--- /dev/null
+++ b/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/JUnitReportFromJMReportCsv.java
@@ -0,0 +1,413 @@
+package io.github.vdaburon.jmeter.utils.reportkpi;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.MissingOptionException;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+
+import org.apache.commons.csv.CSVRecord;
+import org.w3c.dom.Document;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+public class JUnitReportFromJMReportCsv {
+
+ private static final Logger LOGGER = Logger.getLogger(JUnitReportFromJMReportCsv.class.getName());
+ public static final int K_RETURN_OK = 0;
+ public static final int K_RETURN_KO = 1;
+ public static final String K_JUNIT_XML_FILE_DEFAULT = "jmeter-junit-plugin-jmreport.xml";
+ public static final String K_CVS_JM_REPORT_OPT = "csvJMReport";
+ public static final String K_CSV_LABEL_COLUMN_NAME_OPT = "csvLabelColumnName";
+ public static final String K_KPI_FILE_OPT = "kpiFile";
+ public static final String K_JUNIT_XML_FILE_OPT = "junitFile";
+ public static final String K_EXIT_RETURN_ON_FAIL_OPT = "exitReturnOnFail";
+
+
+ // column name for the kpi csv file
+ public static final String K_CSV_COL_NAME_KPI = "name_kpi";
+ public static final String K_CSV_COL_METRIC_CSV_COLUM_NAME = "metric_csv_column_name";
+ public static final String K_CSV_COL_LABEL_REGEX = "label_regex";
+ public static final String K_CSV_COL_COMPARATOR = "comparator";
+ public static final String K_CSV_COL_THREASHOLD = "threshold";
+
+
+ // column name Label in jmeter csv report
+ public static final String K_CSV_JMREPORT_COL_LABEL_DEFAULT = "Label";
+
+ public static final int K_FAIL_MESSAGE_SIZE_MAX = 1024;
+
+ public static void main(String[] args) {
+ long startTimeMs = System.currentTimeMillis();
+
+ Options options = createOptions();
+ Properties parseProperties = null;
+
+ try {
+ parseProperties = parseOption(options, args);
+ } catch (ParseException ex) {
+ helpUsage(options);
+ System.exit(K_RETURN_KO);
+ }
+ int exitReturn = K_RETURN_KO;
+
+ String csvJmeterReport = "NOT SET";
+ String csvLabelColumnName = K_CSV_JMREPORT_COL_LABEL_DEFAULT;
+ String kpiFile = "NOT SET";
+ String junitFile = K_JUNIT_XML_FILE_DEFAULT;
+ boolean exitOnFailKpi = false;
+
+ String sTmp;
+ sTmp = (String) parseProperties.get(K_CVS_JM_REPORT_OPT);
+ if (sTmp != null) {
+ csvJmeterReport = sTmp;
+ }
+
+ sTmp = (String) parseProperties.get(K_CSV_LABEL_COLUMN_NAME_OPT);
+ if (sTmp != null) {
+ csvLabelColumnName = sTmp;
+ }
+
+ sTmp = (String) parseProperties.get(K_KPI_FILE_OPT);
+ if (sTmp != null) {
+ kpiFile = sTmp;
+ }
+
+ sTmp = (String) parseProperties.get(K_JUNIT_XML_FILE_OPT);
+ if (sTmp != null) {
+ junitFile = sTmp;
+ }
+
+ sTmp = (String) parseProperties.get(K_EXIT_RETURN_ON_FAIL_OPT);
+ if (sTmp != null) {
+ exitOnFailKpi = Boolean.parseBoolean(sTmp);
+ LOGGER.fine("exitOnFailKpi:" + exitOnFailKpi);
+ }
+ boolean isKpiFail = false;
+ LOGGER.info("Parameters CLI:" + parseProperties);
+ try {
+ isKpiFail = analyseCsvJMReportWithKpiRules(csvJmeterReport,csvLabelColumnName, kpiFile, junitFile);
+ LOGGER.info("isKpiFail=" + isKpiFail);
+ } catch (Exception ex) {
+ LOGGER.warning(ex.toString());
+ exitReturn = K_RETURN_KO;
+ }
+ if (exitOnFailKpi && isKpiFail) {
+ // at least one kpi rule failure => exit 1
+ exitReturn = K_RETURN_KO;
+ LOGGER.info("exitOnFailKpi=" + exitOnFailKpi + " and isKpiFail=" + isKpiFail + " set program exit=" + exitReturn);
+ } else {
+ exitReturn = K_RETURN_OK;
+ }
+ long endTimeMs = System.currentTimeMillis();
+ LOGGER.info("Duration ms=" + (endTimeMs - startTimeMs));
+ LOGGER.info("End main (exit " + exitReturn + ")");
+
+ System.exit(exitReturn);
+ }
+
+ /**
+ * Analyse the kpi verifications on JMeter report values
+ * @param csvJmeterReport the JMeter Report CSV format
+ * @param csvLabelColumnName the Label Column Name (default : Label)
+ * @param kpiFile the kpi contains kpi declaration
+ * @param junitFile the JUnit XML out file to create
+ * @return is Fail true or false, a kpi is fail or not
+ * @throws IOException file exception
+ * @throws ParserConfigurationException error reading csv file
+ * @throws TransformerException error writing JUnit XML file
+ */
+ private static boolean analyseCsvJMReportWithKpiRules(String csvJmeterReport, String csvLabelColumnName, String kpiFile, String junitFile) throws IOException, ParserConfigurationException, TransformerException {
+ boolean isFail = false;
+ List csvJMReportLines = UtilsCsvFile.readCsvFile(csvJmeterReport);
+ List csvKpiLines = UtilsCsvFile.readCsvFile(kpiFile);
+
+ Document document = UtilsJUnitXml.createJUnitRootDocument();
+ for (int i = 0; i < csvKpiLines.size(); i++) {
+ CheckKpiResult checkKpiResult = verifyKpi(csvKpiLines.get(i), csvJMReportLines, csvLabelColumnName);
+ if (checkKpiResult.isKpiFail()) {
+ isFail = true;
+ String className = checkKpiResult.getMetricCsvColumnName() + " (" + checkKpiResult.getLabelRegex() + ") " + checkKpiResult.getComparator() + " " + checkKpiResult.getThreshold();
+ UtilsJUnitXml.addTestCaseFailure(document,checkKpiResult.getNameKpi(), className, checkKpiResult.getFailMessage());
+ } else {
+ String className = checkKpiResult.getMetricCsvColumnName() + " (" + checkKpiResult.getLabelRegex() + ") " + checkKpiResult.getComparator() + " " + checkKpiResult.getThreshold();
+ UtilsJUnitXml.addTestCaseOk(document,checkKpiResult.getNameKpi(), className);
+ }
+ }
+ LOGGER.info("Write junitFile=" + junitFile);
+ UtilsJUnitXml.saveXmlInFile(document, junitFile);
+ return isFail;
+ }
+
+ /**
+ * verify one kpi for lines in csv JMeter Report
+ * @param recordKpiLine a kpi line to verify
+ * @param csvJMReportLines all lines in JMeter Report
+ * @param csvLabelColumnName the Label Column name in the JMeter Report (usually : Label)
+ * @return the result of the kpi verification and the failure message if kpi fail
+ */
+ private static CheckKpiResult verifyKpi(CSVRecord recordKpiLine, List csvJMReportLines, String csvLabelColumnName) {
+ CheckKpiResult checkKpiResult = new CheckKpiResult();
+ String nameKpi = recordKpiLine.get(K_CSV_COL_NAME_KPI);
+ checkKpiResult.setNameKpi(nameKpi.trim());
+
+ String metricCsvColumnName = recordKpiLine.get(K_CSV_COL_METRIC_CSV_COLUM_NAME);
+ checkKpiResult.setMetricCsvColumnName(metricCsvColumnName.trim());
+
+ String labelRegex = recordKpiLine.get(K_CSV_COL_LABEL_REGEX);
+ checkKpiResult.setLabelRegex(labelRegex);
+
+ String comparator = recordKpiLine.get(K_CSV_COL_COMPARATOR);
+ checkKpiResult.setComparator(comparator.trim());
+
+ String threshold = recordKpiLine.get(K_CSV_COL_THREASHOLD);
+ checkKpiResult.setThreshold(threshold.trim());
+
+ checkKpiResult.setKpiFail(false);
+ checkKpiResult.setFailMessage("NOT SET");
+
+ Pattern patternRegex = Pattern.compile(labelRegex) ;
+
+ boolean isFailKpi = false;
+ boolean isFirstFail = true;
+ for (int i = 0; i < csvJMReportLines.size(); i++) {
+ CSVRecord recordJMReportLine = csvJMReportLines.get(i);
+ String label = recordJMReportLine.get(csvLabelColumnName);
+ Matcher matcherRegex = patternRegex.matcher(label) ;
+ if (matcherRegex.matches()) {
+ String sMetric = recordJMReportLine.get(metricCsvColumnName);
+ LOGGER.fine("sMetric=<" + sMetric + ">");
+ double dMetric = 0;
+ if (sMetric.contains("%")) {
+ sMetric = sMetric.replace('%', ' ');
+ dMetric = Double.parseDouble(sMetric);
+ dMetric = dMetric / 100;
+ } else {
+ dMetric = Double.parseDouble(sMetric);
+ }
+ LOGGER.fine("dMetric=<" + dMetric + ">");
+
+ String sThreshold = checkKpiResult.getThreshold();
+ sThreshold = sThreshold.replace('%', ' ');
+ double dThreshold = Double.parseDouble(sThreshold);
+
+ String sComparator = checkKpiResult.getComparator();
+ switch (sComparator) {
+ case "<":
+ if (dMetric < dThreshold) {
+ LOGGER.fine(dMetric + sComparator + dThreshold);
+ } else {
+ isFailKpi = true;
+ if (isFirstFail) {
+ isFirstFail = false;
+ String failMessage = "Actual value " + dMetric + " exceeds threshold " + dThreshold + " for samples matching \"" + labelRegex + "\"; fail label(s) \"" + label + "\""; // Actual value 2908,480000 exceeds threshold 2500,000000 for samples matching "@SC01_P03_DUMMY"
+ checkKpiResult.setKpiFail(true);
+ checkKpiResult.setFailMessage(failMessage);
+ } else {
+ String failMessage = checkKpiResult.getFailMessage();
+ if ((failMessage.length() + label.length()) < K_FAIL_MESSAGE_SIZE_MAX) {
+ failMessage += ", \"" + label + "\"";
+ } else {
+ if (!failMessage.endsWith(" ...")) {
+ failMessage += " ...";
+ }
+ }
+ checkKpiResult.setFailMessage(failMessage);
+ }
+ }
+ break;
+ case "<=":
+ if (dMetric <= dThreshold) {
+ LOGGER.fine(dMetric + sComparator + dThreshold);
+ } else {
+ isFailKpi = true;
+ if (isFirstFail) {
+ isFirstFail = false;
+ String failMessage = "Actual value " + dMetric + " exceeds or equals threshold " + dThreshold + " for samples matching \"" + labelRegex + "\"; fail label(s) \"" + label + "\"";
+ checkKpiResult.setKpiFail(true);
+ checkKpiResult.setFailMessage(failMessage);
+ } else {
+ String failMessage = checkKpiResult.getFailMessage();
+ if ((failMessage.length() + label.length()) < K_FAIL_MESSAGE_SIZE_MAX) {
+ failMessage += ", \"" + label + "\"";
+ } else {
+ if (!failMessage.endsWith(" ...")) {
+ failMessage += " ...";
+ }
+ }
+ checkKpiResult.setFailMessage(failMessage);
+ }
+ }
+ break;
+ case ">":
+ if (dMetric > dThreshold) {
+ LOGGER.fine(dMetric + sComparator + dThreshold);
+ } else {
+ isFailKpi = true;
+ if (isFirstFail) {
+ isFirstFail = false;
+ String failMessage = "Actual value " + dMetric + " is less then threshold " + dThreshold + " for samples matching \"" + labelRegex + "\"; fail label(s) \"" + label + "\"";
+ checkKpiResult.setKpiFail(true);
+ checkKpiResult.setFailMessage(failMessage);
+ } else {
+ String failMessage = checkKpiResult.getFailMessage();
+ if ((failMessage.length() + label.length()) < K_FAIL_MESSAGE_SIZE_MAX) {
+ failMessage += ", \"" + label + "\"";
+ } else {
+ if (!failMessage.endsWith(" ...")) {
+ failMessage += " ...";
+ }
+ }
+ checkKpiResult.setFailMessage(failMessage);
+ }
+ }
+ break;
+ case ">=":
+ if (dMetric >= dThreshold) {
+ LOGGER.fine(dMetric + sComparator + dThreshold);
+ } else {
+ isFailKpi = true;
+ if (isFirstFail) {
+ isFirstFail = false;
+ String failMessage = "Actual value " + dMetric + "is less or equals threshold " + dThreshold + " for samples matching \"" + labelRegex + "\"; fail label(s) \"" + label + "\"";
+ checkKpiResult.setKpiFail(true);
+ checkKpiResult.setFailMessage(failMessage);
+ } else {
+ String failMessage = checkKpiResult.getFailMessage();
+ if ((failMessage.length() + label.length()) < K_FAIL_MESSAGE_SIZE_MAX) {
+ failMessage += ", \"" + label + "\"";
+ } else {
+ if (!failMessage.endsWith(" ...")) {
+ failMessage += " ...";
+ }
+ }
+ checkKpiResult.setFailMessage(failMessage);
+ }
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid comparator:" + sComparator);
+ }
+ }
+ }
+ return checkKpiResult;
+ }
+
+ /**
+ * If incorrect parameter or help, display usage
+ * @param options options and cli parameters
+ */
+ private static void helpUsage(Options options) {
+ HelpFormatter formatter = new HelpFormatter();
+ String footer = "E.g : java -jar junit-reporter-kpi-from-jmeter-report-csv--jar-with-dependencies.jar -" + K_CVS_JM_REPORT_OPT + " summary.csv -" +
+ K_KPI_FILE_OPT + " kpi.csv -" + K_EXIT_RETURN_ON_FAIL_OPT + " true\n";
+ footer += "or more parameters : java -jar junit-reporter-kpi-from-jmeter-report-csv--jar-with-dependencies.jar -" + K_CVS_JM_REPORT_OPT + " AggregateReport.csv -"
+ + K_CSV_LABEL_COLUMN_NAME_OPT + " Label -" + K_KPI_FILE_OPT + " kpi_check.csv -" + K_JUNIT_XML_FILE_OPT + " junit.xml -" + K_EXIT_RETURN_ON_FAIL_OPT + " true\n";
+ formatter.printHelp(140, JUnitReportFromJMReportCsv.class.getName(),
+ JUnitReportFromJMReportCsv.class.getName(), options, footer, true);
+ }
+
+ /**
+ * Parse options enter in command line interface
+ * @param optionsP parameters to parse
+ * @param args parameters from cli
+ * @return properties saved
+ * @throws ParseException parsing error
+ * @throws MissingOptionException mandatory parameter not set
+ */
+ private static Properties parseOption(Options optionsP, String[] args) throws ParseException, MissingOptionException {
+
+ Properties properties = new Properties();
+
+ CommandLineParser parser = new DefaultParser();
+
+ // parse the command line arguments
+
+ CommandLine line = parser.parse(optionsP, args);
+
+ if (line.hasOption("help")) {
+ properties.setProperty("help", "help value");
+ return properties;
+ }
+
+ if (line.hasOption(K_CVS_JM_REPORT_OPT)) {
+ properties.setProperty(K_CVS_JM_REPORT_OPT, line.getOptionValue(K_CVS_JM_REPORT_OPT));
+ }
+
+ if (line.hasOption(K_CSV_LABEL_COLUMN_NAME_OPT)) {
+ properties.setProperty(K_CSV_LABEL_COLUMN_NAME_OPT, line.getOptionValue(K_CSV_LABEL_COLUMN_NAME_OPT));
+ }
+
+ if (line.hasOption(K_KPI_FILE_OPT)) {
+ properties.setProperty(K_KPI_FILE_OPT, line.getOptionValue(K_KPI_FILE_OPT));
+ }
+
+ if (line.hasOption(K_JUNIT_XML_FILE_OPT)) {
+ properties.setProperty(K_JUNIT_XML_FILE_OPT, line.getOptionValue(K_JUNIT_XML_FILE_OPT));
+ }
+
+ if (line.hasOption(K_EXIT_RETURN_ON_FAIL_OPT)) {
+ properties.setProperty(K_EXIT_RETURN_ON_FAIL_OPT, line.getOptionValue(K_EXIT_RETURN_ON_FAIL_OPT));
+ }
+
+ return properties;
+ }
+ /**
+ * Options or parameters for the command line interface
+ * @return all options
+ **/
+ private static Options createOptions() {
+ Options options = new Options();
+
+ Option helpOpt = Option.builder("help").hasArg(false).desc("Help and show parameters").build();
+
+ options.addOption(helpOpt);
+
+ Option csvJmeterReportFileOpt = Option.builder(K_CVS_JM_REPORT_OPT).argName(K_CVS_JM_REPORT_OPT)
+ .hasArg(true)
+ .required(true)
+ .desc("JMeter report csv file (E.g : summary.csv)")
+ .build();
+ options.addOption(csvJmeterReportFileOpt);
+
+ Option csvLabelColumnNameOpt = Option.builder(K_CSV_LABEL_COLUMN_NAME_OPT).argName(K_CSV_LABEL_COLUMN_NAME_OPT)
+ .hasArg(true)
+ .required(false)
+ .desc("Label Column Name in CSV JMeter Report (Default : " + K_CSV_JMREPORT_COL_LABEL_DEFAULT + ")")
+ .build();
+ options.addOption(csvLabelColumnNameOpt);
+
+ Option kpiFileOpt = Option.builder(K_KPI_FILE_OPT).argName(K_KPI_FILE_OPT)
+ .hasArg(true)
+ .required(true)
+ .desc("KPI file contains rule to check (E.g : kpi.csv)")
+ .build();
+ options.addOption(kpiFileOpt);
+
+ Option junitXmlOutOpt = Option.builder(K_JUNIT_XML_FILE_OPT).argName(K_JUNIT_XML_FILE_OPT)
+ .hasArg(true)
+ .required(false)
+ .desc("junit file name out (Default : " + K_JUNIT_XML_FILE_DEFAULT + ")")
+ .build();
+ options.addOption(junitXmlOutOpt);
+
+ Option exitReturnOnFailOpt = Option.builder(K_EXIT_RETURN_ON_FAIL_OPT).argName(K_EXIT_RETURN_ON_FAIL_OPT)
+ .hasArg(true)
+ .required(false)
+ .desc("if true then when kpi fail then create JUnit XML file and program return exit 1 (KO); if false [Default] then create JUnit XML File and exit 0 (OK)")
+ .build();
+ options.addOption(exitReturnOnFailOpt);
+
+ return options;
+ }
+}
diff --git a/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/UtilsCsvFile.java b/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/UtilsCsvFile.java
new file mode 100644
index 0000000..14a139c
--- /dev/null
+++ b/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/UtilsCsvFile.java
@@ -0,0 +1,29 @@
+package io.github.vdaburon.jmeter.utils.reportkpi;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVRecord;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UtilsCsvFile {
+ /**
+ * Read all lines in a csv file
+ * @param fileIn the csv file name to read
+ * @return a ArrayList of CSVRecord contains all lines
+ * @throws IOException error when read the CSV file
+ */
+ public static List readCsvFile(String fileIn) throws IOException {
+ Reader in = new FileReader(fileIn);
+ Iterable records = CSVFormat.RFC4180.withFirstRecordAsHeader().parse(in);
+
+ List listRecordsBetweenFirstAndLast = new ArrayList();
+
+ for (CSVRecord record : records) {
+ listRecordsBetweenFirstAndLast.add(record);
+ }
+ in.close();
+ return listRecordsBetweenFirstAndLast;
+ }
+}
diff --git a/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/UtilsJUnitXml.java b/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/UtilsJUnitXml.java
new file mode 100644
index 0000000..272e6aa
--- /dev/null
+++ b/src/main/java/io/github/vdaburon/jmeter/utils/reportkpi/UtilsJUnitXml.java
@@ -0,0 +1,163 @@
+package io.github.vdaburon.jmeter.utils.reportkpi;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.File;
+
+/**
+ * Utility Class to create a JUnit DOM, add testcase and write JUnit XML file
+ */
+
+public class UtilsJUnitXml {
+ /**
+ * Create the DOM for a JUnit XML file
+ * @return the DOM with testsuite root element
+ * @throws ParserConfigurationException error creating XML DOM
+ */
+ public static Document createJUnitRootDocument() throws ParserConfigurationException {
+/*
+
+ */
+ DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance();
+
+ DocumentBuilder documentBuilder = documentFactory.newDocumentBuilder();
+
+ Document document = documentBuilder.newDocument();
+
+ Element root = document.createElement("testsuite");
+ document.appendChild(root);
+ Attr attr1 = document.createAttribute("errors");
+ attr1.setValue("0");
+ root.setAttributeNode(attr1);
+
+ Attr attr2 = document.createAttribute("failures");
+ attr2.setValue("0");
+ root.setAttributeNode(attr2);
+
+ Attr attr3 = document.createAttribute("name");
+ attr3.setValue("JUnit Report From JMeter Report Csv");
+ root.setAttributeNode(attr3);
+
+ Attr attr4 = document.createAttribute("skipped");
+ attr4.setValue("0");
+ root.setAttributeNode(attr4);
+
+ Attr attr5 = document.createAttribute("tests");
+ attr5.setValue("0");
+ root.setAttributeNode(attr5);
+
+
+ return document;
+ }
+
+ /**
+ * increment attribute in testsuite (number of tests and number of failures)
+ * @param document the JUnit DOM
+ * @param attribute the attribute name to find and increment current value
+ * @return the value incremented
+ */
+ public static int incrementTestsuiteAttribute(Document document, String attribute) {
+ Element testSuiteElt = document.getDocumentElement();
+ String sValueAttribute = testSuiteElt.getAttributes().getNamedItem(attribute).getTextContent();
+ int iValueAttribute = Integer.parseInt(sValueAttribute);
+ iValueAttribute++;
+ testSuiteElt.getAttributes().getNamedItem(attribute).setTextContent("" + iValueAttribute);
+ return iValueAttribute;
+ }
+
+ /**
+ * Add a Test Case OK
+ * @param document the JUnit DOM
+ * @param classname the name_kpi
+ * @param name the kpi rule + label_regex + comparator + threshold
+ */
+ public static void addTestCaseOk(Document document, String classname, String name) {
+/*
+
+ */
+ Element testcase = document.createElement("testcase");
+ Element testSuiteElt = document.getDocumentElement();
+ testSuiteElt.appendChild(testcase);
+
+ Attr attr1 = document.createAttribute("classname");
+ attr1.setValue(classname);
+ testcase.setAttributeNode(attr1);
+
+ Attr attr2 = document.createAttribute("name");
+ attr2.setValue(name);
+ testcase.setAttributeNode(attr2);
+
+ incrementTestsuiteAttribute(document,"tests");
+
+ }
+
+ /**
+ * Add a Test Case Failure
+ * @param document the JUnit DOM
+ * @param classname the name_kpi
+ * @param name the kpi rule + label_regex + comparator + threshold
+ * @param failureMessage the message explains kpi failure
+ */
+ public static void addTestCaseFailure(Document document, String classname, String name, String failureMessage) {
+/*
+
+ Actual value 3068,200000 exceeds threshold 3000,000000 for samples matching "@SC.*"
+
+ */
+ Element testcase = document.createElement("testcase");
+ Element testSuiteElt = document.getDocumentElement();
+ testSuiteElt.appendChild(testcase);
+
+ Attr attr1 = document.createAttribute("classname");
+ attr1.setValue(classname);
+ testcase.setAttributeNode(attr1);
+
+ Attr attr2 = document.createAttribute("name");
+ attr2.setValue(name);
+ testcase.setAttributeNode(attr2);
+
+ Element failure = document.createElement("failure");
+ Attr attrFailure = document.createAttribute("message");
+ attrFailure.setValue("");
+ failure.setAttributeNode(attrFailure);
+
+ testcase.appendChild(failure);
+ failure.appendChild(document.createTextNode(failureMessage));
+
+ incrementTestsuiteAttribute(document,"tests");
+ incrementTestsuiteAttribute(document,"failures");
+ }
+
+ /**
+ * Save the JUnit DOM in a XML file
+ * @param document JUnit DOM
+ * @param junitXmlFileOut XML file to write
+ * @throws TransformerException error when write XML file
+ */
+ public static void saveXmlInFile(Document document, String junitXmlFileOut) throws TransformerException {
+ // create the xml file
+ //transform the DOM Object to an XML File
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ transformerFactory.setAttribute("indent-number", 3);
+ Transformer transformer = transformerFactory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ DOMSource domSource = new DOMSource(document);
+ StreamResult streamResult = new StreamResult(new File(junitXmlFileOut));
+ transformer.transform(domSource, streamResult);
+ }
+}