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); + } +}