From d4334a9129be75be2060c5354fba34cb55d10ee3 Mon Sep 17 00:00:00 2001 From: rabelenda Date: Mon, 5 Feb 2024 15:39:54 -0300 Subject: [PATCH] Add support for CsvDataSet --- .../Abstracta.JmeterDsl.Tests.csproj | 15 +- .../Core/Configs/DslCsvDataSetTest.cs | 22 ++ .../Core/Configs/data.csv | 3 + .../Core/Configs/DslCsvDataSet.cs | 218 ++++++++++++++++++ Abstracta.JmeterDsl/JmeterDsl.cs | 28 +++ docs/guide/request-generation/csv-dataset.md | 67 ++++++ docs/guide/request-generation/index.md | 1 + .../loops/forloop-controller.md | 2 +- pom.xml | 2 +- 9 files changed, 344 insertions(+), 14 deletions(-) create mode 100644 Abstracta.JmeterDsl.Tests/Core/Configs/DslCsvDataSetTest.cs create mode 100644 Abstracta.JmeterDsl.Tests/Core/Configs/data.csv create mode 100644 Abstracta.JmeterDsl/Core/Configs/DslCsvDataSet.cs create mode 100644 docs/guide/request-generation/csv-dataset.md diff --git a/Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj b/Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj index 63d7234..6eedd4e 100644 --- a/Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj +++ b/Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj @@ -28,21 +28,12 @@ - - - - - - - - - - - - PreserveNewest + + PreserveNewest + \ No newline at end of file diff --git a/Abstracta.JmeterDsl.Tests/Core/Configs/DslCsvDataSetTest.cs b/Abstracta.JmeterDsl.Tests/Core/Configs/DslCsvDataSetTest.cs new file mode 100644 index 0000000..611ffa3 --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/Core/Configs/DslCsvDataSetTest.cs @@ -0,0 +1,22 @@ +namespace Abstracta.JmeterDsl.Core.Configs +{ + using static JmeterDsl; + + public class DslCsvDataSetTest + { + [Test] + public void ShouldGetExpectedSamplesWhenTestPlanWithSampleNamesFromCsvDataSet() + { + var stats = TestPlan( + CsvDataSet("Core/Configs/data.csv"), + ThreadGroup(1, 2, + DummySampler("${VAR1}-${VAR2}", "ok") + )).Run(); + Assert.Multiple(() => + { + Assert.That(stats.Labels["val1-val2"].SamplesCount, Is.EqualTo(1)); + Assert.That(stats.Labels["val,3-val4"].SamplesCount, Is.EqualTo(1)); + }); + } + } +} diff --git a/Abstracta.JmeterDsl.Tests/Core/Configs/data.csv b/Abstracta.JmeterDsl.Tests/Core/Configs/data.csv new file mode 100644 index 0000000..5c4154d --- /dev/null +++ b/Abstracta.JmeterDsl.Tests/Core/Configs/data.csv @@ -0,0 +1,3 @@ +VAR1,VAR2 +val1,"val2" +"val,3",val4 diff --git a/Abstracta.JmeterDsl/Core/Configs/DslCsvDataSet.cs b/Abstracta.JmeterDsl/Core/Configs/DslCsvDataSet.cs new file mode 100644 index 0000000..a10f0ad --- /dev/null +++ b/Abstracta.JmeterDsl/Core/Configs/DslCsvDataSet.cs @@ -0,0 +1,218 @@ +using System.Text; + +namespace Abstracta.JmeterDsl.Core.Configs +{ + /// + /// Allows using a CSV file as input data for JMeter variables to use in test plan. + ///
+ /// This element reads a CSV file and uses each line to generate JMeter variables to be used in each + /// iteration and thread of the test plan. + ///
+ /// Is ideal to be able to easily create test plans that test with a lot of different of potential + /// requests or flows. + ///
+ /// By default, it consumes comma separated variables, which names are included in first line of CSV, + /// automatically resets to the beginning of the file when the end is reached and the consumption of + /// the file is shared by all threads and thread groups in the test plan (ie: any iteration on a + /// thread will consume a line from the file, and advance to following line). + ///
+ /// Additionally, this element sets by default the "quoted data" flag on JMeter CSV Data Set + /// element. + ///
+ public class DslCsvDataSet : BaseConfigElement + { + private readonly string _csvFile; + private string _delimiter; + private string _encoding; + private string[] _variableNames; + private bool? _ignoreFirstLine; + private bool? _stopThreadOnEOF; + private Sharing? _sharedIn; + private bool? _randomOrder; + + public DslCsvDataSet(string csvFile) + : base(null) + { + _csvFile = csvFile; + } + + /// + /// Specifies the way the threads in a test plan consume the CSV. + /// + public enum Sharing + { + /// + /// All threads in the test plan will share the CSV file, meaning that any thread iteration will + /// consume an entry from it. You can think as having only one pointer to the current line of the + /// CSV, being advanced by any thread iteration. The file is only opened once. + /// + AllThreads, + + /// + /// CSV file consumption is only shared within thread groups. This means that threads in separate + /// thread groups will use separate indexes to consume the data. The file is open once per thread + /// group. + /// + ThreadGroup, + + /// + /// CSV file consumption is isolated per thread. This means that each thread will start consuming + /// the CSV from the beginning and not share any information with other threads. The file is open + /// once per thread. + /// + Thread, + } + + /// + /// Specifies the delimiter used by the file to separate variable values. + /// + /// specifies the delimiter. By default, it uses commas (,) as delimiters. If you need to use tabs, then specify "\\t". + /// the dataset for further configuration or usage. + public DslCsvDataSet Delimiter(string delimiter) + { + _delimiter = delimiter; + return this; + } + + /// + /// Specifies the file encoding used by the file. + ///
+ /// This method is useful when specifying a dynamic encoding (through JMeter variable or function + /// reference). Otherwise prefer using . + ///
+ /// the file encoding of the file. By default, it will use UTF-8 (which differs + /// from JMeter default, to have more consistent test plan execution). This might + /// require to be changed but in general is good to have all files in same encoding + /// (eg: UTF-8). + /// the dataset for further configuration or usage. + public DslCsvDataSet Encoding(string encoding) + { + _encoding = encoding; + return this; + } + + /// + /// Specifies the file encoding used by the file. + ///
+ /// If you need to specify a dynamic encoding (through JMeter variable or function reference), then + /// use instead. + ///
+ /// the file encoding of the file. By default, it will use UTF-8 (which differs + /// from JMeter default, to have more consistent test plan execution). This might + /// require to be changed but in general is good to have all files in same encoding + /// (eg: UTF-8). + /// the dataset for further configuration or usage. + public DslCsvDataSet Encoding(Encoding encoding) + { + _encoding = encoding.EncodingName; + return this; + } + + /// + /// Specifies variable names to be assigned to the parsed values. + ///
+ /// If you have a CSV file with existing headers and want to overwrite the name of generated + /// variables, then use in conjunction with this method to specify the + /// new variable names. If you have a CSV file without a headers line, then you will need to use + /// this method to set proper names for the variables (otherwise first line of data will be used as + /// headers, which will not be good). + ///
+ /// names of variables to be extracted from the CSV file. + /// the dataset for further configuration or usage. + public DslCsvDataSet VariableNames(params string[] variableNames) + { + _variableNames = variableNames; + return this; + } + + /// + /// Specifies to ignore first line of the CSV. + ///
+ /// This should only be used in conjunction with to overwrite + /// existing CSV headers names. + ///
+ /// the dataset for further configuration or usage. + public DslCsvDataSet IgnoreFirstLine() + => IgnoreFirstLine(true); + + /// + /// Same as but allowing to enable or disable it. + ///
+ /// This is helpful when the resolution is taken at runtime. + ///
+ /// specifies to enable or disable the setting. By default, it is set to false. + /// the dataset for further configuration or usage. + /// + public DslCsvDataSet IgnoreFirstLine(bool enable) + { + _ignoreFirstLine = enable; + return this; + } + + /// + /// Specifies to stop threads when end of given CSV file is reached. + ///
+ /// This method will automatically internally set JMeter test element property "recycle on EOF", so + /// you don't need to worry about such property. + ///
+ /// the dataset for further configuration or usage. + public DslCsvDataSet StopThreadOnEOF() + => StopThreadOnEOF(true); + + /// + /// Same as but allowing to enable or disable it. + ///
+ /// This is helpful when the resolution is taken at runtime. + ///
+ /// specifies to enable or disable the setting. By default, it is set to false. + /// the dataset for further configuration or usage. + /// + public DslCsvDataSet StopThreadOnEOF(bool enable) + { + _stopThreadOnEOF = enable; + return this; + } + + /// + /// Allows changing the way CSV file is consumed (shared) by threads. + /// + /// specifies the way threads consume information from the CSV file. By default, + /// all threads share the CSV information, meaning that any thread iteration will + /// advance the consumption of the file (the file is a singleton). When + /// is used, THREAD_GROUP shared mode is not supported. + /// the dataset for further configuration or usage. + /// + public DslCsvDataSet SharedIn(Sharing shareMode) + { + _sharedIn = shareMode; + return this; + } + + /// + /// Specifies to get file lines in random order instead of sequentially iterating over them. + ///
+ /// When this method is invoked Random CSV Data Set plugin is used. + ///
+ /// Warning: Getting lines in random order has a performance penalty. + ///
+ /// Warning: When random order is enabled, share mode THREAD_GROUP is not supported. + ///
+ /// the dataset for further configuration or usage. + public DslCsvDataSet RandomOrder() + => RandomOrder(true); + + /// + /// Same as but allowing to enable or disable it. + ///
+ /// This is helpful when the resolution is taken at runtime. + ///
+ /// specifies to enable or disable the setting. By default, it is set to false. + /// the dataset for further configuration or usage. + /// + public DslCsvDataSet RandomOrder(bool enable) + { + _randomOrder = enable; + return this; + } + } +} diff --git a/Abstracta.JmeterDsl/JmeterDsl.cs b/Abstracta.JmeterDsl/JmeterDsl.cs index 6f7f406..cc2c8ab 100644 --- a/Abstracta.JmeterDsl/JmeterDsl.cs +++ b/Abstracta.JmeterDsl/JmeterDsl.cs @@ -1,5 +1,6 @@ using System; using Abstracta.JmeterDsl.Core; +using Abstracta.JmeterDsl.Core.Configs; using Abstracta.JmeterDsl.Core.Controllers; using Abstracta.JmeterDsl.Core.Listeners; using Abstracta.JmeterDsl.Core.PostProcessors; @@ -322,5 +323,32 @@ public static ResponseFileSaver ResponseFileSaver(string fileNamePrefix) => /// public static ResultsTreeVisualizer ResultsTreeVisualizer() => new ResultsTreeVisualizer(); + + /// + /// Builds a CSV Data Set which allows loading from a CSV file variables to be used in test plan. + ///
+ /// This allows to store for example in a CSV file one line for each user credentials, and then in + /// the test plan be able to use all the credentials to test with different users. + ///
+ /// By default, the CSV data set will read comma separated values, use first row as name of the + /// generated variables, restart from beginning when csv entries are exhausted and will read a new + /// line of CSV for each thread and iteration. + ///
+ /// E.g: If you have a csv with 2 entries and a test plan with two threads, iterating 2 times each, + /// you might get (since threads run in parallel, the assignment is not deterministic) following + /// assignment of rows: + ///
+ ///
+        /// thread 1, row 1
+        /// thread 2, row 2
+        /// thread 2, row 1
+        /// thread 1, row 2
+        /// 
+ ///
+ /// path to the CSV file to read the data from. + /// the CSV Data Set instance for further configuration and usage. + /// + public static DslCsvDataSet CsvDataSet(string csvFile) => + new DslCsvDataSet(csvFile); } } diff --git a/docs/guide/request-generation/csv-dataset.md b/docs/guide/request-generation/csv-dataset.md new file mode 100644 index 0000000..1b6f4a5 --- /dev/null +++ b/docs/guide/request-generation/csv-dataset.md @@ -0,0 +1,67 @@ +### CSV as input data for requests + +Sometimes is necessary to run the same flow but using different pre-defined data on each request. For example, a common use case is to use a different user (from a given set) in each request. + +This can be easily achieved using the provided `CsvDataSet` element. For example, having a file like this one: + +```csv +USER,PASS +user1,pass1 +user2,pass2 +``` + +You can implement a test plan that tests recurrent login with the two users with something like this: + +```cs +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using static Abstracta.JmeterDsl.JmeterDsl; + +public class PerformanceTest +{ + [Test] + public void LoadTest() + { + var stats = TestPlan( + CsvDataSet("users.csv"), + ThreadGroup(5, 10, + HttpSampler("http://my.service/login") + .Post("{\"${USER}\": \"${PASS}\"", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)), + HttpSampler("http://my.service/logout") + .Method(HttpMethod.Post.Method) + ) + ).Run(); + Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); + } +} +``` + +::: tip +By default, the CSV file will be opened once and shared by all threads. This means that when one thread reads a CSV line in one iteration, then the following thread reading a line will continue with the following line. + +If you want to change this (to share the file per thread group or use one file per thread), then you can use the provided `SharedIn` method like in the following example: + +```java +using static Abstracta.JmeterDsl.Core.Configs.DslCsvDataSet; +... + var stats = TestPlan( + CsvDataSet("users.csv") + .SharedIn(Sharing.Thread), + ThreadGroup(5, 10, + HttpSampler("http://my.service/login") + .Post("{\"${USER}\": \"${PASS}\"", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)), + HttpSampler("http://my.service/logout") + .Method(HttpMethod.Post.Method) + ) + ).Run(); + Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); +``` +::: + +::: warning +You can use the `RandomOrder()` method to get CSV lines in random order (using [Random CSV Data Set plugin](https://github.com/Blazemeter/jmeter-bzm-plugins/blob/master/random-csv-data-set/RandomCSVDataSetConfig.md)), but this is less performant as getting them sequentially, so use it sparingly. +::: + +Check [DslCsvDataSet](/Abstracta.JmeterDsl/Core/Configs/DslCsvDataSet.cs) for additional details and options (like changing delimiter, handling files without headers line, stopping on the end of file, etc.). diff --git a/docs/guide/request-generation/index.md b/docs/guide/request-generation/index.md index 27be7a1..03ad6dc 100644 --- a/docs/guide/request-generation/index.md +++ b/docs/guide/request-generation/index.md @@ -1,3 +1,4 @@ ## Requests generation + diff --git a/docs/guide/request-generation/loops/forloop-controller.md b/docs/guide/request-generation/loops/forloop-controller.md index 92817be..80e5587 100644 --- a/docs/guide/request-generation/loops/forloop-controller.md +++ b/docs/guide/request-generation/loops/forloop-controller.md @@ -1,6 +1,6 @@ #### Iterating a fixed number of times -In simple scenarios where you just want to execute a fixed number of times, within a thread group iteration, a given part of the test plan, you can just use `forLoopController` (which uses [JMeter Loop Controller component](https://jmeter.apache.org/usermanual/component_reference.html#Loop_Controller)) as in the following example: +In simple scenarios where you just want to execute a fixed number of times, within a thread group iteration, a given part of the test plan, you can just use `ForLoopController` (which uses [JMeter Loop Controller component](https://jmeter.apache.org/usermanual/component_reference.html#Loop_Controller)) as in the following example: ```cs using static Abstracta.JmeterDsl.JmeterDsl; diff --git a/pom.xml b/pom.xml index 708df55..1bf3031 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,6 @@ This pom is only needed to be able to copy jmeter-java-dsl jars and dependencies with dependency:copy-dependencies maven plugin goal in child projects - 1.25.1 + 1.25.2 \ No newline at end of file