diff --git a/404.html b/404.html new file mode 100644 index 0000000..92a308a --- /dev/null +++ b/404.html @@ -0,0 +1,38 @@ + + +
+ + + + + +0?R.reduce(function(q,ae){return[].concat(q,el(ae))},[]):k;return se.forEach(function(q){q.classList.remove("medium-zoom-image"),q.dispatchEvent(Cn("medium-zoom:detach",{detail:{zoom:H}}))}),k=k.filter(function(q){return se.indexOf(q)===-1}),H},f=function(P,R){var $=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return k.forEach(function(se){se.addEventListener("medium-zoom:"+P,R,$)}),C.push({type:"medium-zoom:"+P,listener:R,options:$}),H},d=function(P,R){var $=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return k.forEach(function(se){se.removeEventListener("medium-zoom:"+P,R,$)}),C=C.filter(function(se){return!(se.type==="medium-zoom:"+P&&se.listener.toString()===R.toString())}),H},g=function(){var P=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},R=P.target,$=function(){var q={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},ae=void 0,ie=void 0;if(L.container)if(L.container instanceof Object)q=on({},q,L.container),ae=q.width-q.left-q.right-L.margin*2,ie=q.height-q.top-q.bottom-L.margin*2;else{var je=ra(L.container)?L.container:document.querySelector(L.container),De=je.getBoundingClientRect(),Ve=De.width,Be=De.height,Mt=De.left,Ft=De.top;q=on({},q,{width:Ve,height:Be,left:Mt,top:Ft})}ae=ae||q.width-L.margin*2,ie=ie||q.height-L.margin*2;var vt=b.zoomedHd||b.original,Fe=Qs(vt)?ae:vt.naturalWidth||ae,S=Qs(vt)?ie:vt.naturalHeight||ie,U=vt.getBoundingClientRect(),D=U.top,G=U.left,me=U.width,m=U.height,h=Math.min(Math.max(me,Fe),ae)/me,y=Math.min(Math.max(m,S),ie)/m,E=Math.min(h,y),A=(-G+(ae-me)/2+L.margin+q.left)/E,O=(-D+(ie-m)/2+L.margin+q.top)/E,j="scale("+E+") translate3d("+A+"px, "+O+"px, 0)";b.zoomed.style.transform=j,b.zoomedHd&&(b.zoomedHd.style.transform=j)};return new r(function(se){if(R&&k.indexOf(R)===-1){se(H);return}var q=function Ve(){N=!1,b.zoomed.removeEventListener("transitionend",Ve),b.original.dispatchEvent(Cn("medium-zoom:opened",{detail:{zoom:H}})),se(H)};if(b.zoomed){se(H);return}if(R)b.original=R;else if(k.length>0){var ae=k;b.original=ae[0]}else{se(H);return}if(b.original.dispatchEvent(Cn("medium-zoom:open",{detail:{zoom:H}})),K=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,N=!0,b.zoomed=jb(b.original),document.body.appendChild(F),L.template){var ie=ra(L.template)?L.template:document.querySelector(L.template);b.template=document.createElement("div"),b.template.appendChild(ie.content.cloneNode(!0)),document.body.appendChild(b.template)}if(b.original.parentElement&&b.original.parentElement.tagName==="PICTURE"&&b.original.currentSrc&&(b.zoomed.src=b.original.currentSrc),document.body.appendChild(b.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),b.original.classList.add("medium-zoom-image--hidden"),b.zoomed.classList.add("medium-zoom-image--opened"),b.zoomed.addEventListener("click",_),b.zoomed.addEventListener("transitionend",q),b.original.getAttribute("data-zoom-src")){b.zoomedHd=b.zoomed.cloneNode(),b.zoomedHd.removeAttribute("srcset"),b.zoomedHd.removeAttribute("sizes"),b.zoomedHd.removeAttribute("loading"),b.zoomedHd.src=b.zoomed.getAttribute("data-zoom-src"),b.zoomedHd.onerror=function(){clearInterval(je),console.warn("Unable to reach the zoom image target "+b.zoomedHd.src),b.zoomedHd=null,$()};var je=setInterval(function(){b.zoomedHd.complete&&(clearInterval(je),b.zoomedHd.classList.add("medium-zoom-image--opened"),b.zoomedHd.addEventListener("click",_),document.body.appendChild(b.zoomedHd),$())},10)}else if(b.original.hasAttribute("srcset")){b.zoomedHd=b.zoomed.cloneNode(),b.zoomedHd.removeAttribute("sizes"),b.zoomedHd.removeAttribute("loading");var De=b.zoomedHd.addEventListener("load",function(){b.zoomedHd.removeEventListener("load",De),b.zoomedHd.classList.add("medium-zoom-image--opened"),b.zoomedHd.addEventListener("click",_),document.body.appendChild(b.zoomedHd),$()})}else $()})},_=function(){return new r(function(P){if(N||!b.original){P(H);return}var R=function $(){b.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(b.zoomed),b.zoomedHd&&document.body.removeChild(b.zoomedHd),document.body.removeChild(F),b.zoomed.classList.remove("medium-zoom-image--opened"),b.template&&document.body.removeChild(b.template),N=!1,b.zoomed.removeEventListener("transitionend",$),b.original.dispatchEvent(Cn("medium-zoom:closed",{detail:{zoom:H}})),b.original=null,b.zoomed=null,b.zoomedHd=null,b.template=null,P(H)};N=!0,document.body.classList.remove("medium-zoom--opened"),b.zoomed.style.transform="",b.zoomedHd&&(b.zoomedHd.style.transform=""),b.template&&(b.template.style.transition="opacity 150ms",b.template.style.opacity=0),b.original.dispatchEvent(Cn("medium-zoom:close",{detail:{zoom:H}})),b.zoomed.addEventListener("transitionend",R)})},w=function(){var P=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},R=P.target;return b.original?_():g({target:R})},x=function(){return L},p=function(){return k},v=function(){return b.original},k=[],C=[],N=!1,K=0,L=n,b={original:null,zoomed:null,zoomedHd:null,template:null};Object.prototype.toString.call(t)==="[object Object]"?L=t:(t||typeof t=="string")&&c(t),L=on({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},L);var F=Hb(L.background);document.addEventListener("click",a),document.addEventListener("keyup",i),document.addEventListener("scroll",o),window.addEventListener("resize",_);var H={open:g,close:_,toggle:w,update:s,clone:l,attach:c,detach:u,on:f,off:d,getOptions:x,getImages:p,getZoomedImage:v};return H};function Ub(e,t){t===void 0&&(t={});var n=t.insertAt;if(!(!e||typeof document>"u")){var r=document.head||document.getElementsByTagName("head")[0],a=document.createElement("style");a.type="text/css",n==="top"&&r.firstChild?r.insertBefore(a,r.firstChild):r.appendChild(a),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(document.createTextNode(e))}}var Wb=".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}";Ub(Wb);const qb=Bb,Kb=Symbol("mediumZoom");const Yb="*:is(img):not(.card img):not(a img)",Vb={},Gb=500,Xb=ht({enhance({app:e,router:t}){const n=qb(Vb);n.refresh=(r=Yb)=>{n.detach(),n.attach(r)},e.provide(Kb,n),t.afterEach(()=>{setTimeout(()=>n.refresh(),Gb)})}});function tl(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),n.push.apply(n,r)}return n}function W(e){for(var t=1;t JMeter DSL has received valuable support from industry-leading companies, contributing to the integration features and promoting the tool. We would like to acknowledge and express our gratitude to the following companies: To use the DSL just include it in your project: To generate HTTP requests just use provided The following example uses 2 threads (concurrent users) that send 10 HTTP GET requests each to Additionally, it logs collected statistics (response times, status codes, etc.) to a file (for later analysis if needed) and checks that the response time 99 percentile is less than 5 seconds. TIP When working with multiple samplers in a test plan, specify their names (eg: TIP JMeter .Net DSL uses Java for executing JMeter test plans. If you need to tune JVM parameters, for example for specifying maximum heap memory size, you can use TIP Depending on the test framework you use, and the way you run your tests, you might be able to see JMeter logs and output in real-time, at the end of the test, or not see them at all. This is not something we can directly control in JMeter DSL, and heavily depends on the dotnet environment and testing framework implementation. When using Nunit, to get real-time console output from JMeter you might want to run your tests with something like And using the provided engine like this: WARNING By default, the engine is configured to time out if test execution takes more than 1 hour. This timeout exists to avoid any potential problem with Azure Load Testing execution not detected by the client, and avoid keeping the test indefinitely running until is interrupted by a user, which may incur unnecessary expenses in Azure and is especially annoying when running tests in an automated fashion, for example in CI/CD. It is strongly advised to set this timeout properly in each run, according to the expected test execution time plus some additional margin (to consider for additional delays in Azure Load Testing test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or unnecessary waits when there is some unexpected issue with Azure Load Testing execution. TIP If you want to get debug logs for HTTP calls to Azure API, you can include the following setting to an existing By including the following package: This test is using WARNING By default the engine is configured to timeout if test execution takes more than 1 hour. This timeout exists to avoid any potential problem with BlazeMeter execution not detected by the client, and avoid keeping the test indefinitely running until is interrupted by a user, which may incur in unnecessary expenses in BlazeMeter and is specially annoying when running tests in automated fashion, for example in CI/CD. It is strongly advised to set this timeout properly in each run, according to the expected test execution time plus some additional margin (to consider for additional delays in BlazeMeter test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or unnecessary waits when there is some unexpected issue with BlazeMeter execution. WARNING TIP In case you want to get debug logs for HTTP calls to BlazeMeter API, you can include the following setting to an existing JMeter DSL provides two simple ways of creating thread groups which are used in most scenarios: This is how they look in code: But these options are not good when working with many threads or when trying to configure some complex test scenarios (like when doing incremental or peak tests). When working with many threads, it is advisable to configure a ramp-up period, to avoid starting all threads at once affecting performance metrics and generation. You can easily configure a ramp-up with the DSL like this: Additionally, you can use and combine these same methods to configure more complex scenarios (incremental, peak, and any other types of tests) like the following one: Which would translate into the following threads' timeline: TIP If you are a JMeter GUI user, you may even be interested in using provided For example, for the above test plan you would get a window like the following one: TIP When using multiple thread groups in a test plan, consider setting a name (eg: A usual requirement while building a test plan is to be able to review requests and responses and debug the test plan for potential issues in the configuration or behavior of the service under test. With JMeter DSL you have several options for this purpose. One option is using provided This will display the JMeter built-in View Results Tree element, which allows you to review request and response contents in addition to collected metrics (spent time, sent & received bytes, etc.) for each request sent to the server, in a window like this one: TIP To debug test plans use a few iterations and threads to reduce the execution time and ease tracing by having less information to analyze. TIP When adding TIP Remove WARNING By default, View Results Tree only displays the last 500 sample results. If you need to display more elements, use provided You can even add breakpoints to JMeter or JMeter Java DSL code in your IDE and debug the code line by line providing the greatest possible detail. Here is an example screenshot debugging HTTP Sampler: For that, you need to: Note that we changed the TIP JMeter class in charge of executing threads logic is Here is an example: TIP The DSL configures dummy samplers by default, in contrast to what JMeter does, with response time simulation disabled. This allows to speed up the debugging process, not having to wait for proper response time simulation (sleeps/waits). If you want a more accurate emulation, you might turn it on through the A usual requirement for new DSL users that are used to Jmeter GUI, is to be able to review Jmeter DSL generated test plan in the familiar JMeter GUI. For this, you can use the This can be also used to debug the test plan, by adding elements (like view results tree, dummy samplers, etc.) in the GUI and running the test plan. Here is a simple example using the method: Which ends up opening a window like this one: The main mechanism provided by JMeter (and This can be easily achieved by using provided TIP If you need a specific file name, for example for later postprocessing logic (eg: using CI build ID), you can specify it by using When specifying the file name, make sure to use unique names, otherwise, the JTL contents may be appended to previous existing jtl files. An additional option, specially targeted towards logging sample responses, is It is a usual requirement while creating a test plan for an application to be able to use part of a response (e.g.: a generated ID, token, etc.) in a subsequent request. This can be easily achieved using JMeter extractors and variables. Here is an example with JMeter DSL using regular expressions: This will result in 10 * 5 = 50 requests to the given URL for each thread in the thread group. TIP JMeter automatically generates a variable 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 You can implement a test plan that tests recurrent login with the two users with something like this: 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 So far we have seen a how to generate requests with information extracted from CSV, but this is not enough for some scenarios. When you need more flexibility and power you can use Here is an example: As previously seen, you can do simple gets and posts like in the following snippet: But you can also use additional methods to specify any HTTP method and body: In many cases, you will need to specify some URL query string parameters or URL encoded form bodies. For these cases, you can use TIP JMeter automatically URL encodes parameters, so you don't need to worry about special characters in parameter names or values. If you want to use some custom encoding or have an already encoded value that you want to use, then you can use You might have already noticed in some of the examples that we have shown, some ways to set some headers. For instance, in the following snippet, These are handy methods to specify the Additionally, you can specify headers to be used by all samplers in a test plan, thread group, transaction controllers, etc. For this, you can use When you need to upload files to an HTTP server or need to send a complex request body, you will in many cases require sending multipart requests. To send a multipart request just use JMeter DSL automatically adds a cookie manager and cache manager for automatic HTTP cookie and caching handling, emulating a browser behavior. If you need to disable them you can use something like this: Add the package to your project: Create performance test: Java 8+ is required for test plan execution. Here we share some tips and examples on how to use the DSL to tackle common use cases. Provided examples use Nunit, but you can use other test libraries. Explore the DSL in your preferred IDE to discover all available features, and consider reviewing existing tests for additional examples. The .Net DSL currently does not support all use cases supported by the Java Dsl, and currently only focuses on a limited set of features that cover the most commonly used cases. If you identify any particular scenario (or JMeter feature) that you need and is not currently supported, or easy to use, please let us know by creating an issue and we will try to implement it as soon as possible. Usually porting JMeter features is quite fast, and porting existing Java DSL features is even faster. TIP If you like this project, please give it a star ⭐ in GitHub!. This helps the project be more visible, gain relevance and encourages us to invest more effort in new features. For an intro to JMeter concepts and components, you can check JMeter official documentation. To use the DSL just include it in your project: TIP Here is a sample project in case you want to start one from scratch. WARNING JMeter .Net DSL uses existing JMeter Java DSL which in turn uses JMeter. JMeter Java DSL and JMeter are Java based tools. So, Java 8+ is required for the proper execution of DSL test plans. One option is downloading a JVM from Adoptium if you don't have one already. To generate HTTP requests just use provided The following example uses 2 threads (concurrent users) that send 10 HTTP GET requests each to Additionally, it logs collected statistics (response times, status codes, etc.) to a file (for later analysis if needed) and checks that the response time 99 percentile is less than 5 seconds. TIP When working with multiple samplers in a test plan, specify their names (eg: TIP JMeter .Net DSL uses Java for executing JMeter test plans. If you need to tune JVM parameters, for example for specifying maximum heap memory size, you can use TIP Since JMeter uses log4j2, if you want to control the logging level or output, you can use something similar to this log4j2.xml, using "CopyToOutputDirectory" in the project item, so the file is available in dotnet build output directory as well (check [Abstracta.JmeterDsl.Test/Abstracta.JmeterDsl.Tests.csproj]). TIP Depending on the test framework you use, and the way you run your tests, you might be able to see JMeter logs and output in real-time, at the end of the test, or not see them at all. This is not something we can directly control in JMeter DSL, and heavily depends on the dotnet environment and testing framework implementation. When using Nunit, to get real-time console output from JMeter you might want to run your tests with something like TIP Keep in mind that you can use .Net programming to modularize and create abstractions which allow you to build complex test plans that are still easy to read, use and maintain. Here is an example of some complex abstraction built using Java features (you can easily extrapolate to .Net) and the DSL. Check HTTP performance testing for additional details while testing HTTP services. Running a load test from one machine is not always enough, since you are limited to the machine's hardware capabilities. Sometimes, is necessary to run the test using a cluster of machines to be able to generate enough load for the system under test. Currently, the .Net DSL only provides two ways to run tests at scale, but in the future, we plan to support more (as Java DSL does). If you are interested in some not yet covered feature, please ask for it by creating an issue in the repository. To use Azure Load Testing to execute your test plans at scale is as easy as including the following package to your project: And using the provided engine like this: This test is using With Azure, you can not only run the test at scale but also get additional features like nice real-time reporting, historic data tracking, etc. Here is an example of how a test looks like in Azure Load Testing: Check AzureEngine for details on usage and available settings when running tests in Azure Load Testing. WARNING By default, the engine is configured to time out if test execution takes more than 1 hour. This timeout exists to avoid any potential problem with Azure Load Testing execution not detected by the client, and avoid keeping the test indefinitely running until is interrupted by a user, which may incur unnecessary expenses in Azure and is especially annoying when running tests in an automated fashion, for example in CI/CD. It is strongly advised to set this timeout properly in each run, according to the expected test execution time plus some additional margin (to consider for additional delays in Azure Load Testing test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or unnecessary waits when there is some unexpected issue with Azure Load Testing execution. TIP If you want to get debug logs for HTTP calls to Azure API, you can include the following setting to an existing By including the following package: You can easily run a JMeter test plan at scale in BlazeMeter like this: This test is using Note that is as simple as generating a BlazeMeter authentication token and adding BlazeMeter will not only allow you to run the test at scale but also provides additional features like nice real-time reporting, historic data tracking, etc. Here is an example of how a test would look in BlazeMeter: Check BlazeMeterEngine for details on usage and available settings when running tests in BlazeMeter. WARNING By default the engine is configured to timeout if test execution takes more than 1 hour. This timeout exists to avoid any potential problem with BlazeMeter execution not detected by the client, and avoid keeping the test indefinitely running until is interrupted by a user, which may incur in unnecessary expenses in BlazeMeter and is specially annoying when running tests in automated fashion, for example in CI/CD. It is strongly advised to set this timeout properly in each run, according to the expected test execution time plus some additional margin (to consider for additional delays in BlazeMeter test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or unnecessary waits when there is some unexpected issue with BlazeMeter execution. WARNING TIP In case you want to get debug logs for HTTP calls to BlazeMeter API, you can include the following setting to an existing JMeter DSL provides two simple ways of creating thread groups which are used in most scenarios: This is how they look in code: But these options are not good when working with many threads or when trying to configure some complex test scenarios (like when doing incremental or peak tests). When working with many threads, it is advisable to configure a ramp-up period, to avoid starting all threads at once affecting performance metrics and generation. You can easily configure a ramp-up with the DSL like this: Additionally, you can use and combine these same methods to configure more complex scenarios (incremental, peak, and any other types of tests) like the following one: Which would translate into the following threads' timeline: Check DslThreadGroup for more details. TIP If you are a JMeter GUI user, you may even be interested in using provided For example, for the above test plan you would get a window like the following one: TIP When using multiple thread groups in a test plan, consider setting a name (eg: A usual requirement while building a test plan is to be able to review requests and responses and debug the test plan for potential issues in the configuration or behavior of the service under test. With JMeter DSL you have several options for this purpose. One option is using provided This will display the JMeter built-in View Results Tree element, which allows you to review request and response contents in addition to collected metrics (spent time, sent & received bytes, etc.) for each request sent to the server, in a window like this one: TIP To debug test plans use a few iterations and threads to reduce the execution time and ease tracing by having less information to analyze. TIP When adding TIP Remove WARNING By default, View Results Tree only displays the last 500 sample results. If you need to display more elements, use provided You can even add breakpoints to JMeter or JMeter Java DSL code in your IDE and debug the code line by line providing the greatest possible detail. Here is an example screenshot debugging HTTP Sampler: For that, you need to: Note that we changed the TIP JMeter class in charge of executing threads logic is In many cases, you want to be able to test part of the test plan but without directly interacting with the service under test, avoiding any potential traffic to the servers, testing some border cases which might be difficult to reproduce with the actual server, and avoid actual server interactions variability and potential unpredictability. In such scenarios, you might replace actual samplers with Here is an example: TIP The DSL configures dummy samplers by default, in contrast to what JMeter does, with response time simulation disabled. This allows to speed up the debugging process, not having to wait for proper response time simulation (sleeps/waits). If you want a more accurate emulation, you might turn it on through the Check DslDummySampler for more information o additional configuration and options. A usual requirement for new DSL users that are used to Jmeter GUI, is to be able to review Jmeter DSL generated test plan in the familiar JMeter GUI. For this, you can use the This can be also used to debug the test plan, by adding elements (like view results tree, dummy samplers, etc.) in the GUI and running the test plan. Here is a simple example using the method: Which ends up opening a window like this one: Once you have a test plan you would usually want to be able to analyze the collected information. This section contains a few ways to achieve this, but in the future, we plan to support more (as Java DSL does). If you are interested in some not yet covered feature, please ask for it by creating an issue in the repository. The main mechanism provided by JMeter (and This can be easily achieved by using provided TIP By default, TIP If you need a specific file name, for example for later postprocessing logic (eg: using CI build ID), you can specify it by using When specifying the file name, make sure to use unique names, otherwise, the JTL contents may be appended to previous existing jtl files. An additional option, specially targeted towards logging sample responses, is Check ResponseFileSaver for more details. It is a usual requirement while creating a test plan for an application to be able to use part of a response (e.g.: a generated ID, token, etc.) in a subsequent request. This can be easily achieved using JMeter extractors and variables. Here is an example with JMeter DSL using regular expressions: Check DslRegexExtractor for more details and additional options. 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 This will result in 10 * 5 = 50 requests to the given URL for each thread in the thread group. TIP JMeter automatically generates a variable Check ForLoopController for more details. 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 You can implement a test plan that tests recurrent login with the two users with something like this: 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 WARNING You can use the Check DslCsvDataSet for additional details and options (like changing delimiter, handling files without headers line, stopping on the end of file, etc.). So far we have seen a how to generate requests with information extracted from CSV, but this is not enough for some scenarios. When you need more flexibility and power you can use Here is an example: TIP For the time being only JSR223 scripts can be used. By default We plan in the future to look for alternatives as to be able to use .Net code as pre processor. If you are interested in this you can let us know in this issue posting your use case. Check DslJsr223PreProcessor for more details and additional options. Throughout this guide, several examples have been shown for simple cases of HTTP requests (mainly how to do gets and posts), but the DSL provides additional features that you might need to be aware of. Here we show some of them, but check JmeterDsl and DslHttpSampler to explore all available features. As previously seen, you can do simple gets and posts like in the following snippet: But you can also use additional methods to specify any HTTP method and body: In many cases, you will need to specify some URL query string parameters or URL encoded form bodies. For these cases, you can use TIP JMeter automatically URL encodes parameters, so you don't need to worry about special characters in parameter names or values. If you want to use some custom encoding or have an already encoded value that you want to use, then you can use You might have already noticed in some of the examples that we have shown, some ways to set some headers. For instance, in the following snippet, These are handy methods to specify the Additionally, you can specify headers to be used by all samplers in a test plan, thread group, transaction controllers, etc. For this, you can use When you need to upload files to an HTTP server or need to send a complex request body, you will in many cases require sending multipart requests. To send a multipart request just use JMeter DSL automatically adds a cookie manager and cache manager for automatic HTTP cookie and caching handling, emulating a browser behavior. If you need to disable them you can use something like this: Industry Support
Setup
dotnet add package Abstracta.JmeterDsl --version 0.5
+
Simple HTTP test plan
HttpSampler
.http://my.service
.using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ ),
+ //this is just to log details of each request stats
+ JtlWriter("jtls")
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
HttpSampler("home", "http://my.service")
) to easily check their respective statistics.EmbeddedJMeterEngine
and the JvmArgs
method like in the following example:using Abstracta.JmeterDsl.Core.Engines;
+...
+var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+).RunIn(new EmbeddedJmeterEngine()
+ .JvmArgs("-Xmx4g")
+);
+
dotnet test -v n
and add the following code to your tests:private TextWriter? originalConsoleOut;
+
+// Redirecting output to progress to get live stdout with nunit.
+// https://github.com/nunit/nunit3-vs-adapter/issues/343
+// https://github.com/nunit/nunit/issues/1139
+[SetUp]
+public void SetUp()
+{
+ originalConsoleOut = Console.Out;
+ Console.SetOut(TestContext.Progress);
+}
+
+[TearDown]
+public void TearDown()
+{
+ Console.SetOut(originalConsoleOut!);
+}
+
dotnet add package Abstracta.JmeterDsl.Azure --version 0.5
+
using Abstracta.JmeterDsl.Azure;
+using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+ ).RunIn(new AzureEngine(Environment.GetEnvironmentVariable("AZURE_CREDS")) // AZURE_CREDS=tenantId:clientId:secretId
+ .TestName("dsl-test")
+ /*
+ This specifies the number of engine instances used to execute the test plan.
+ In this case, means that it will run 2(threads in thread group)x2(engines)=4 concurrent users/threads in total.
+ Each engine executes the test plan independently.
+ */
+ .Engines(2)
+ .TestTimeout(TimeSpan.FromMinutes(20)));
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
log4j2.xml
configuration file:<Logger name="us.abstracta.jmeter.javadsl.azure.AzureClient" level="DEBUG"/>
+<Logger name="okhttp3" level="DEBUG"/>
+
BlazeMeter
dotnet add package Abstracta.JmeterDsl.BlazeMeter --version 0.5
+
using Abstracta.JmeterDsl.BlazeMeter;
+using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ // number of threads and iterations are in the end overwritten by BlazeMeter engine settings
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+ ).RunIn(new BlazeMeterEngine(Environment.GetEnvironmentVariable("BZ_TOKEN"))
+ .TestName("DSL test")
+ .TotalUsers(500)
+ .HoldFor(TimeSpan.FromMinutes(10))
+ .ThreadsPerEngine(100)
+ .TestTimeout(TimeSpan.FromMinutes(20))
+ .TestName("dsl-test"));
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
`,2),ln={href:"https://guide.blazemeter.com/hc/en-us/articles/115002213289-BlazeMeter-API-keys-",target:"_blank",rel:"noopener noreferrer"},un=s("code",null,".RunIn(new BlazeMeterEngine(...))",-1),rn=s("p",null,"BlazeMeter will not only allow you to run the test at scale but also provides additional features like nice real-time reporting, historic data tracking, etc. Here is an example of how a test would look in BlazeMeter:",-1),dn=s("p",null,[s("img",{src:u,alt:"BlazeMeter Example Execution Dashboard"})],-1),kn={href:"https://github.com/abstracta/jmeter-dotnet-dsl/tree/master/Abstracta.JmeterDsl.BlazeMeter/BlazeMeterEngine.cs",target:"_blank",rel:"noopener noreferrer"},mn=e(`BZ_TOKEN
, a custom environment variable with <KEY_ID>:<KEY_SECRET>
format, to get the BlazeMeter API authentication credentials.BlazeMeterEngine
always returns 0 as sentBytes
statistics since there is no efficient way to get it from BlazMeter.log4j2.xml
configuration file:<Logger name="us.abstracta.jmeter.javadsl.blazemeter.BlazeMeterClient" level="DEBUG"/>
+<Logger name="okhttp3" level="DEBUG"/>
+
Advanced threads configuration
ThreadGroup(10, 20, ...) // 10 threads for 20 iterations each
+ThreadGroup(10, TimeSpan.FromSeconds(20), ...) // 10 threads for 20 seconds each
+
Thread ramps and holds
ThreadGroup().RampTo(10, TimeSpan.FromSeconds(5)).HoldIterating(20) // ramp to 10 threads for 5 seconds (1 thread every half second) and iterating each thread 20 times
+ThreadGroup().RampToAndHold(10, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(20)) //similar as above but after ramping up holding execution for 20 seconds
+
ThreadGroup()
+ .RampToAndHold(10, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(20))
+ .RampToAndHold(100, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30))
+ .RampTo(200, TimeSpan.FromSeconds(10))
+ .RampToAndHold(100, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30))
+ .RampTo(0, TimeSpan.FromSeconds(5))
+ .Children(
+ HttpSampler("http://my.service")
+ )
+
TestElement.ShowInGui()
method, which shows the JMeter test element GUI that could help you understand what will DSL execute in JMeter. You can use this method with any test element generated by the DSL (not just thread groups).ThreadGroup("main", 1, 1, ...)
) on them to properly identify associated requests in statistics & jtl results. Test plan debugging
View results tree
ResultsTreeVisualizer()
like in the following example:using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ ),
+ ResultsTreeVisualizer()
+ ).Run();
+ }
+}
+
ResultsTreeVisualizer()
as a child of a thread group, it will only display sample results of that thread group. When added as a child of a sampler, it will only show sample results for that sampler. You can use this to only review certain sample results in your test plan.ResultsTreeVisualizer()
from test plans when are no longer needed (when debugging is finished). Leaving them might interfere with unattended test plan execution (eg: in CI) due to test plan execution not finishing until all visualizers windows are closed.ResultsLimit(int)
method which allows changing this value. Take into consideration that the more results are shown, the more memory that will require. So use this setting with care. Debug JMeter code
EmbeddedJmeterEngine
like in the following example:TestPlan(
+ ThreadGroup(threads: 1, iterations: 1,
+ HttpSampler("http://my.service")
+ )
+ ).RunIn(new EmbeddedJmeterEngine()
+ .JvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"));
+
suspend
flag to y
to block test execution until Remote JVM Debug is run in IDE.org.apache.jmeter.threads.JMeterThread
. You can check the classes used by each DSL-provided test element by checking the Java DSL code.`,21),bn=s("code",null,"DummySampler",-1),gn={href:"https://jmeter-plugins.org/wiki/DummySampler/",target:"_blank",rel:"noopener noreferrer"},fn=e(` Dummy sampler
using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ TestPlan(
+ ThreadGroup(2, 10,
+ // HttpSampler("http://my.service")
+ DummySampler("{\\"status\\" : \\"OK\\"}")
+ )
+ ).Run();
+ }
+}
+
ResponseTimeSimulation()
method. Test plan review un JMeter GUI
ShowInGui()
method in a test plan to open JMeter GUI with the preloaded test plan.using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+ ).ShowInGui();
+ }
+}
+
',8),Tn={href:"https://abstracta.github.io/jmeter-java-dsl/guide/#reporting",target:"_blank",rel:"noopener noreferrer"},_n={href:"https://github.com/abstracta/jmeter-dotnet-dsl/issues",target:"_blank",rel:"noopener noreferrer"},qn=e(` Reporting
Log requests and responses
Abstracta.JmeterDsl
) to get information about generated requests, responses, and associated metrics is through the generation of JTL files.JtlWriter
like in this example:using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ ),
+ JtlWriter("jtls")
+ ).Run();
+ }
+}
+
JtlWriter
will automatically generate .jtl
files applying this format: <yyyy-MM-dd HH-mm-ss> <UUID>.jtl
.JtlWriter(directory, fileName)
.ResponseFileSaver
which automatically generates a file for each received response. Here is an example:using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ ),
+ ResponseFileSaver(DateTime.Now.ToString("dd-MM-yyyy HH:mm:ss").Replace(":", "-") + "-response")
+ ).Run();
+ }
+}
+
Response processing
Use part of a response in a subsequent request (aka correlation)
Regular expressions extraction
using System.Net.Http.Headers;
+using System.Net.Mime;
+
+using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service/accounts")
+ .Post("{\\"name\\": \\"John Doe\\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
+ .Children(
+ RegexExtractor("ACCOUNT_ID", "\\"id\\":\\"([^\\"]+)\\"")
+ ),
+ HttpSampler("http://my.service/accounts/\${ACCOUNT_ID}")
+ )
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
Requests generation
Loops
',3),Rn=s("code",null,"ForLoopController",-1),zn={href:"https://jmeter.apache.org/usermanual/component_reference.html#Loop_Controller",target:"_blank",rel:"noopener noreferrer"},En=e(` Iterating a fixed number of times
using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ ForLoopController(5,
+ HttpSampler("http://my.service/accounts")
+ )
+ )
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
__jm__<loopName>__idx
with the current index of for loop iteration (starting with 0) which you can use in children elements. The default name for the for loop controller, when not specified, is for
. CSV as input data for requests
CsvDataSet
element. For example, having a file like this one:USER,PASS
+user1,pass1
+user2,pass2
+
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)));
+ }
+}
+
SharedIn
method like in the following example: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)));
+
Provide request parameters programmatically per request
jsr223preProcessor
to specify your own logic to build each request.using System;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(5, 10,
+ HttpSampler("http://my.service")
+ .Post("\${REQUEST_BODY}", new MediaTypeHeaderValue(MediaTypeNames.Text.Plain))
+ .Children(Jsr223PreProcessor("vars.put('REQUEST_BODY', '{\\"time\\": \\"' + Instant.now() + '\\"}')"))
+ )
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
Methods & body
HttpSampler("http://my.service") // A simple get
+HttpSampler("http://my.service")
+ .Post("{\\"field\\":\\"val\\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) // simple post
+
HttpSampler("http://my.service")
+ .Method(HttpMethod.Put.Method)
+ .ContentType(new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
+ .Body("{\\"field\\":\\"val\\"}")
+
Parameters
Param
method as in the following example:using static Abstracta.JmeterDsl.JmeterDsl;
+using System.Net.Http;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var baseUrl = "https://myservice.com/products";
+ TestPlan(
+ ThreadGroup(1, 1,
+ // GET https://myservice.com/products?name=iron+chair
+ HttpSampler("GetIronChair", baseUrl)
+ .Param("name", "iron chair"),
+ /*
+ * POST https://myservice.com/products
+ * Content-Type: application/x-www-form-urlencoded
+ *
+ * name=wooden+chair
+ */
+ HttpSampler("CreateWoodenChair", baseUrl)
+ .Method(HttpMethod.Post.Method) // POST
+ .Param("name", "wooden chair")
+ )
+ ).Run();
+ }
+}
+
RawParam
method instead which does not apply any encoding to the parameter name or value, and send it as is. Headers
Content-Type
header is being set in two different ways:HttpSampler("http://my.service")
+ .Post("{\\"field\\":\\"val\\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
+HttpSampler("http://my.service")
+ .ContentType(new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
+
Content-Type
header, but you can also set any header on a particular request using provided Header
method, like this:HttpSampler("http://my.service")
+ .Header("X-First-Header", "val1")
+ .Header("X-Second-Header", "val2")
+
HttpHeaders
like this:TestPlan(
+ ThreadGroup(2, 10,
+ HttpHeaders()
+ .Header("X-Header", "val1"),
+ HttpSampler("http://my.service"),
+ HttpSampler("http://my.service/users")
+ )
+).Run();
+
Multipart requests
BodyPart
and BodyFilePart
methods like in the following example:using static Abstracta.JmeterDsl.JmeterDsl;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ TestPlan(
+ ThreadGroup(1, 1,
+ HttpSampler("https://myservice.com/report"),
+ .Method(HttpMethod.Post.Method)
+ .BodyPart("myText", "Hello World", new MediaTypeHeaderValue(MediaTypeNames.Text.Plain))
+ .BodyFilePart("myFile", "myReport.xml", new MediaTypeHeaderValue(MediaTypeNames.Text.Xml))
+ )
+ ).Run();
+ }
+}
+
Cookies & caching
TestPlan(
+ HttpCookies().Disable(),
+ HttpCache().Disable(),
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+)
+
Example
dotnet add package Abstracta.JmeterDsl --version 0.5
+
using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
`,6),r={href:"https://github.com/abstracta/jmeter-dotnet-dsl-sample",target:"_blank",rel:"noopener noreferrer"};function d(k,m){const a=t("ExternalLinkIcon");return p(),c("div",null,[u,n("p",null,[n("a",r,[s("Here"),o(a)]),s(" is a sample project in case you want to start one from scratch.")])])}const b=e(i,[["render",d],["__file","index.html.vue"]]);export{b as default};
diff --git a/assets/intellij-remote-jvm-debug-448b6207.png b/assets/intellij-remote-jvm-debug-448b6207.png
new file mode 100644
index 0000000..e4bfedf
Binary files /dev/null and b/assets/intellij-remote-jvm-debug-448b6207.png differ
diff --git a/assets/jmeter-http-sampler-debugging-ebeea624.png b/assets/jmeter-http-sampler-debugging-ebeea624.png
new file mode 100644
index 0000000..388fbd8
Binary files /dev/null and b/assets/jmeter-http-sampler-debugging-ebeea624.png differ
diff --git a/assets/octoperf-logo-dc518d38.png b/assets/octoperf-logo-dc518d38.png
new file mode 100644
index 0000000..d39525e
Binary files /dev/null and b/assets/octoperf-logo-dc518d38.png differ
diff --git a/assets/search-0782d0d1.svg b/assets/search-0782d0d1.svg
new file mode 100644
index 0000000..03d8391
--- /dev/null
+++ b/assets/search-0782d0d1.svg
@@ -0,0 +1 @@
+
diff --git a/assets/style-287f3d2a.css b/assets/style-287f3d2a.css
new file mode 100644
index 0000000..b6ef70c
--- /dev/null
+++ b/assets/style-287f3d2a.css
@@ -0,0 +1 @@
+:root{--back-to-top-z-index: 5;--back-to-top-color: #3eaf7c;--back-to-top-color-hover: #71cda3}.back-to-top{cursor:pointer;position:fixed;bottom:2rem;right:2.5rem;width:2rem;height:1.2rem;background-color:var(--back-to-top-color);-webkit-mask:url(/jmeter-dotnet-dsl/assets/back-to-top-8efcbe56.svg) no-repeat;mask:url(/jmeter-dotnet-dsl/assets/back-to-top-8efcbe56.svg) no-repeat;z-index:var(--back-to-top-z-index)}.back-to-top:hover{background-color:var(--back-to-top-color-hover)}@media (max-width: 959px){.back-to-top{display:none}}@media print{.back-to-top{display:none}}.back-to-top-enter-active,.back-to-top-leave-active{transition:opacity .3s}.back-to-top-enter-from,.back-to-top-leave-to{opacity:0}:root{--external-link-icon-color: #aaa}.external-link-icon{position:relative;display:inline-block;color:var(--external-link-icon-color);vertical-align:middle;top:-1px}@media print{.external-link-icon{display:none}}.external-link-icon-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}:root{--nprogress-color: #29d;--nprogress-z-index: 1031}#nprogress{pointer-events:none}#nprogress .bar{background:var(--nprogress-color);position:fixed;z-index:var(--nprogress-z-index);top:0;left:0;width:100%;height:2px}:root{--c-brand: #3eaf7c;--c-brand-light: #4abf8a;--c-bg: #ffffff;--c-bg-light: #f3f4f5;--c-bg-lighter: #eeeeee;--c-bg-dark: #ebebec;--c-bg-darker: #e6e6e6;--c-bg-navbar: var(--c-bg);--c-bg-sidebar: var(--c-bg);--c-bg-arrow: #cccccc;--c-text: #2c3e50;--c-text-accent: var(--c-brand);--c-text-light: #3a5169;--c-text-lighter: #4e6e8e;--c-text-lightest: #6a8bad;--c-text-quote: #999999;--c-border: #eaecef;--c-border-dark: #dfe2e5;--c-tip: #42b983;--c-tip-bg: var(--c-bg-light);--c-tip-title: var(--c-text);--c-tip-text: var(--c-text);--c-tip-text-accent: var(--c-text-accent);--c-warning: #ffc310;--c-warning-bg: #fffae3;--c-warning-bg-light: #fff3ba;--c-warning-bg-lighter: #fff0b0;--c-warning-border-dark: #f7dc91;--c-warning-details-bg: #fff5ca;--c-warning-title: #f1b300;--c-warning-text: #746000;--c-warning-text-accent: #edb100;--c-warning-text-light: #c1971c;--c-warning-text-quote: #ccab49;--c-danger: #f11e37;--c-danger-bg: #ffe0e0;--c-danger-bg-light: #ffcfde;--c-danger-bg-lighter: #ffc9c9;--c-danger-border-dark: #f1abab;--c-danger-details-bg: #ffd4d4;--c-danger-title: #ed1e2c;--c-danger-text: #660000;--c-danger-text-accent: #bd1a1a;--c-danger-text-light: #b5474d;--c-danger-text-quote: #c15b5b;--c-details-bg: #eeeeee;--c-badge-tip: var(--c-tip);--c-badge-warning: #ecc808;--c-badge-warning-text: var(--c-bg);--c-badge-danger: #dc2626;--c-badge-danger-text: var(--c-bg);--t-color: .3s ease;--t-transform: .3s ease;--code-bg-color: #282c34;--code-hl-bg-color: rgba(0, 0, 0, .66);--code-ln-color: #9e9e9e;--code-ln-wrapper-width: 3.5rem;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;--font-family-code: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;--navbar-height: 3.6rem;--navbar-padding-v: .7rem;--navbar-padding-h: 1.5rem;--sidebar-width: 20rem;--sidebar-width-mobile: calc(var(--sidebar-width) * .82);--content-width: 740px;--homepage-width: 960px}.back-to-top{--back-to-top-color: var(--c-brand);--back-to-top-color-hover: var(--c-brand-light)}.DocSearch{--docsearch-primary-color: var(--c-brand);--docsearch-text-color: var(--c-text);--docsearch-highlight-color: var(--c-brand);--docsearch-muted-color: var(--c-text-quote);--docsearch-container-background: rgba(9, 10, 17, .8);--docsearch-modal-background: var(--c-bg-light);--docsearch-searchbox-background: var(--c-bg-lighter);--docsearch-searchbox-focus-background: var(--c-bg);--docsearch-searchbox-shadow: inset 0 0 0 2px var(--c-brand);--docsearch-hit-color: var(--c-text-light);--docsearch-hit-active-color: var(--c-bg);--docsearch-hit-background: var(--c-bg);--docsearch-hit-shadow: 0 1px 3px 0 var(--c-border-dark);--docsearch-footer-background: var(--c-bg)}.external-link-icon{--external-link-icon-color: var(--c-text-quote)}.medium-zoom-overlay{--medium-zoom-bg-color: var(--c-bg)}#nprogress{--nprogress-color: var(--c-brand)}.pwa-popup{--pwa-popup-text-color: var(--c-text);--pwa-popup-bg-color: var(--c-bg);--pwa-popup-border-color: var(--c-brand);--pwa-popup-shadow: 0 4px 16px var(--c-brand);--pwa-popup-btn-text-color: var(--c-bg);--pwa-popup-btn-bg-color: var(--c-brand);--pwa-popup-btn-hover-bg-color: var(--c-brand-light)}.search-box{--search-bg-color: var(--c-bg);--search-accent-color: var(--c-brand);--search-text-color: var(--c-text);--search-border-color: var(--c-border);--search-item-text-color: var(--c-text-lighter);--search-item-focus-bg-color: var(--c-bg-light)}html.dark{--c-brand: #3aa675;--c-brand-light: #349469;--c-bg: #22272e;--c-bg-light: #2b313a;--c-bg-lighter: #262c34;--c-bg-dark: #343b44;--c-bg-darker: #37404c;--c-text: #adbac7;--c-text-light: #96a7b7;--c-text-lighter: #8b9eb0;--c-text-lightest: #8094a8;--c-border: #3e4c5a;--c-border-dark: #34404c;--c-tip: #318a62;--c-warning: #e0ad15;--c-warning-bg: #2d2f2d;--c-warning-bg-light: #423e2a;--c-warning-bg-lighter: #44442f;--c-warning-border-dark: #957c35;--c-warning-details-bg: #39392d;--c-warning-title: #fdca31;--c-warning-text: #d8d96d;--c-warning-text-accent: #ffbf00;--c-warning-text-light: #ddb84b;--c-warning-text-quote: #ccab49;--c-danger: #fc1e38;--c-danger-bg: #39232c;--c-danger-bg-light: #4b2b35;--c-danger-bg-lighter: #553040;--c-danger-border-dark: #a25151;--c-danger-details-bg: #482936;--c-danger-title: #fc2d3b;--c-danger-text: #ea9ca0;--c-danger-text-accent: #fd3636;--c-danger-text-light: #d9777c;--c-danger-text-quote: #d56b6b;--c-details-bg: #323843;--c-badge-warning: var(--c-warning);--c-badge-warning-text: #3c2e05;--c-badge-danger: var(--c-danger);--c-badge-danger-text: #401416;--code-hl-bg-color: #363b46}html.dark .DocSearch{--docsearch-logo-color: var(--c-text);--docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;--docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d, 0 2px 2px 0 rgba(3, 4, 9, .3);--docsearch-key-gradient: linear-gradient(-225deg, #444950, #1c1e21);--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2)}html,body{padding:0;margin:0;background-color:var(--c-bg);transition:background-color var(--t-color)}html.dark{color-scheme:dark}html{font-size:16px}body{font-family:var(--font-family);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:1rem;color:var(--c-text)}a{font-weight:500;color:var(--c-text-accent);text-decoration:none;overflow-wrap:break-word}p a code{font-weight:400;color:var(--c-text-accent)}kbd{font-family:var(--font-family-code);color:var(--c-text);background:var(--c-bg-lighter);border:solid .15rem var(--c-border-dark);border-bottom:solid .25rem var(--c-border-dark);border-radius:.15rem;padding:0 .15em}code{font-family:var(--font-family-code);color:var(--c-text-lighter);padding:.25rem .5rem;margin:0;font-size:.85em;background-color:var(--c-bg-light);border-radius:3px;overflow-wrap:break-word;transition:background-color var(--t-color)}blockquote{font-size:1rem;color:var(--c-text-quote);border-left:.2rem solid var(--c-border-dark);margin:1rem 0;padding:.25rem 0 .25rem 1rem;overflow-wrap:break-word}blockquote>p{margin:0}ul,ol{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25;overflow-wrap:break-word}h1:focus-visible,h2:focus-visible,h3:focus-visible,h4:focus-visible,h5:focus-visible,h6:focus-visible{outline:none}h1:hover .header-anchor,h2:hover .header-anchor,h3:hover .header-anchor,h4:hover .header-anchor,h5:hover .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2.2rem}h2{font-size:1.65rem;padding-bottom:.3rem;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color)}h3{font-size:1.35rem}h4{font-size:1.15rem}h5{font-size:1.05rem}h6{font-size:1rem}a.header-anchor{font-size:.85em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:.125em;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media print{a.header-anchor{display:none}}a.header-anchor:hover{text-decoration:none}a.header-anchor:focus-visible{opacity:1}@media print{a[href^="http://"]:after,a[href^="https://"]:after{content:" (" attr(href) ") "}}p,ul,ol{line-height:1.7;overflow-wrap:break-word}hr{border:0;border-top:1px solid var(--c-border)}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto;transition:border-color var(--t-color)}tr{border-top:1px solid var(--c-border-dark);transition:border-color var(--t-color)}tr:nth-child(2n){background-color:var(--c-bg-light);transition:background-color var(--t-color)}tr:nth-child(2n) code{background-color:var(--c-bg-dark)}th,td{padding:.6em 1em;border:1px solid var(--c-border-dark);transition:border-color var(--t-color)}.arrow{display:inline-block;width:0;height:0}.arrow.up{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:6px solid var(--c-bg-arrow)}.arrow.down{border-left:4px solid transparent;border-right:4px solid transparent;border-top:6px solid var(--c-bg-arrow)}.arrow.right{border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--c-bg-arrow)}.arrow.left{border-top:4px solid transparent;border-bottom:4px solid transparent;border-right:6px solid var(--c-bg-arrow)}.badge{display:inline-block;font-size:14px;font-weight:600;height:18px;line-height:18px;border-radius:3px;padding:0 6px;color:var(--c-bg);vertical-align:top;transition:color var(--t-color),background-color var(--t-color)}.badge.tip{background-color:var(--c-badge-tip)}.badge.warning{background-color:var(--c-badge-warning);color:var(--c-badge-warning-text)}.badge.danger{background-color:var(--c-badge-danger);color:var(--c-badge-danger-text)}.badge+.badge{margin-left:5px}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:var(--font-family-code);font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata{color:#999}.token.punctuation{color:#ccc}.token.tag,.token.attr-name,.token.namespace,.token.deleted{color:#ec5975}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}.token.important,.token.bold{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:#3eaf7c}.theme-default-content pre,.theme-default-content pre[class*=language-]{line-height:1.375;padding:1.3rem 1.5rem;margin:.85rem 0;border-radius:6px;overflow:auto}.theme-default-content pre code,.theme-default-content pre[class*=language-] code{color:#fff;padding:0;background-color:transparent!important;border-radius:0;overflow-wrap:unset;-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.theme-default-content .line-number{font-family:var(--font-family-code)}div[class*=language-]{position:relative;background-color:var(--code-bg-color);border-radius:6px}div[class*=language-]:before{content:attr(data-ext);position:absolute;z-index:3;top:.8em;right:1em;font-size:.75rem;color:var(--code-ln-color)}div[class*=language-] pre,div[class*=language-] pre[class*=language-]{background:transparent!important;position:relative;z-index:1}div[class*=language-] .highlight-lines{-webkit-user-select:none;-moz-user-select:none;user-select:none;padding-top:1.3rem;position:absolute;top:0;left:0;width:100%;line-height:1.375}div[class*=language-] .highlight-lines .highlight-line{background-color:var(--code-hl-bg-color)}div[class*=language-]:not(.line-numbers-mode) .line-numbers{display:none}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line{position:relative}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line:before{content:" ";position:absolute;z-index:2;left:0;top:0;display:block;width:var(--code-ln-wrapper-width);height:100%}div[class*=language-].line-numbers-mode pre{margin-left:var(--code-ln-wrapper-width);padding-left:1rem;vertical-align:middle}div[class*=language-].line-numbers-mode .line-numbers{position:absolute;top:0;width:var(--code-ln-wrapper-width);text-align:center;color:var(--code-ln-color);padding-top:1.25rem;line-height:1.375;counter-reset:line-number}div[class*=language-].line-numbers-mode .line-numbers .line-number{position:relative;z-index:3;-webkit-user-select:none;-moz-user-select:none;user-select:none;height:1.375em}div[class*=language-].line-numbers-mode .line-numbers .line-number:before{counter-increment:line-number;content:counter(line-number);font-size:.85em}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;top:0;left:0;width:var(--code-ln-wrapper-width);height:100%;border-radius:6px 0 0 6px;border-right:1px solid var(--code-hl-bg-color)}@media (max-width: 419px){.theme-default-content div[class*=language-]{margin:.85rem -1.5rem;border-radius:0}}.code-group__nav{margin-top:.85rem;margin-bottom:calc(-1.7rem - 6px);padding-bottom:calc(1.7rem - 6px);padding-left:10px;padding-top:10px;border-top-left-radius:6px;border-top-right-radius:6px;background-color:var(--code-bg-color)}.code-group__ul{margin:auto 0;padding-left:0;display:inline-flex;list-style:none}.code-group__nav-tab{border:0;padding:5px;cursor:pointer;background-color:transparent;font-size:.85em;line-height:1.4;color:#ffffffe6;font-weight:600}.code-group__nav-tab:focus{outline:none}.code-group__nav-tab:focus-visible{outline:1px solid rgba(255,255,255,.9)}.code-group__nav-tab-active{border-bottom:var(--c-brand) 1px solid}@media (max-width: 419px){.code-group__nav{margin-left:-1.5rem;margin-right:-1.5rem;border-radius:0}}.code-group-item{display:none}.code-group-item__active{display:block}.code-group-item>pre{background-color:orange}.custom-container{transition:color var(--t-color),border-color var(--t-color),background-color var(--t-color)}.custom-container .custom-container-title{font-weight:600}.custom-container .custom-container-title:not(:only-child){margin-bottom:-.4rem}.custom-container.tip,.custom-container.warning,.custom-container.danger{padding:.1rem 1.5rem;border-left-width:.5rem;border-left-style:solid;margin:1rem 0}.custom-container.tip{border-color:var(--c-tip);background-color:var(--c-tip-bg);color:var(--c-tip-text)}.custom-container.tip .custom-container-title{color:var(--c-tip-title)}.custom-container.tip a{color:var(--c-tip-text-accent)}.custom-container.tip code{background-color:var(--c-bg-dark)}.custom-container.warning{border-color:var(--c-warning);background-color:var(--c-warning-bg);color:var(--c-warning-text)}.custom-container.warning .custom-container-title{color:var(--c-warning-title)}.custom-container.warning a{color:var(--c-warning-text-accent)}.custom-container.warning blockquote{border-left-color:var(--c-warning-border-dark);color:var(--c-warning-text-quote)}.custom-container.warning code{color:var(--c-warning-text-light);background-color:var(--c-warning-bg-light)}.custom-container.warning details{background-color:var(--c-warning-details-bg)}.custom-container.warning details code{background-color:var(--c-warning-bg-lighter)}.custom-container.warning .external-link-icon{--external-link-icon-color: var(--c-warning-text-quote)}.custom-container.danger{border-color:var(--c-danger);background-color:var(--c-danger-bg);color:var(--c-danger-text)}.custom-container.danger .custom-container-title{color:var(--c-danger-title)}.custom-container.danger a{color:var(--c-danger-text-accent)}.custom-container.danger blockquote{border-left-color:var(--c-danger-border-dark);color:var(--c-danger-text-quote)}.custom-container.danger code{color:var(--c-danger-text-light);background-color:var(--c-danger-bg-light)}.custom-container.danger details{background-color:var(--c-danger-details-bg)}.custom-container.danger details code{background-color:var(--c-danger-bg-lighter)}.custom-container.danger .external-link-icon{--external-link-icon-color: var(--c-danger-text-quote)}.custom-container.details{display:block;position:relative;border-radius:2px;margin:1.6em 0;padding:1.6em;background-color:var(--c-details-bg)}.custom-container.details code{background-color:var(--c-bg-darker)}.custom-container.details h4{margin-top:0}.custom-container.details figure:last-child,.custom-container.details p:last-child{margin-bottom:0;padding-bottom:0}.custom-container.details summary{outline:none;cursor:pointer}.home{padding:var(--navbar-height) 2rem 0;max-width:var(--homepage-width);margin:0 auto;display:block}.home .hero{text-align:center}.home .hero img{max-width:100%;max-height:280px;display:block;margin:3rem auto 1.5rem}.home .hero h1{font-size:3rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.8rem auto}.home .hero .actions{display:flex;flex-wrap:wrap;gap:1rem;justify-content:center}.home .hero .description{max-width:35rem;font-size:1.6rem;line-height:1.3;color:var(--c-text-lightest)}.home .hero .action-button{display:inline-block;font-size:1.2rem;padding:.8rem 1.6rem;border-width:2px;border-style:solid;border-radius:4px;transition:background-color var(--t-color);box-sizing:border-box}.home .hero .action-button.primary{color:var(--c-bg);background-color:var(--c-brand);border-color:var(--c-brand)}.home .hero .action-button.primary:hover{background-color:var(--c-brand-light)}.home .hero .action-button.secondary{color:var(--c-brand);background-color:var(--c-bg);border-color:var(--c-brand)}.home .hero .action-button.secondary:hover{color:var(--c-bg);background-color:var(--c-brand-light)}.home .features{border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding:1.2rem 0;margin-top:2.5rem;display:flex;flex-wrap:wrap;align-items:flex-start;align-content:stretch;justify-content:space-between}.home .feature{flex-grow:1;flex-basis:30%;max-width:30%}.home .feature h2{font-size:1.4rem;font-weight:500;border-bottom:none;padding-bottom:0;color:var(--c-text-light)}.home .feature p{color:var(--c-text-lighter)}.home .theme-default-content{padding:0;margin:0}.home .footer{padding:2.5rem;border-top:1px solid var(--c-border);text-align:center;color:var(--c-text-lighter);transition:border-color var(--t-color)}@media (max-width: 719px){.home .features{flex-direction:column}.home .feature{max-width:100%;padding:0 2.5rem}}@media (max-width: 419px){.home{padding-left:1.5rem;padding-right:1.5rem}.home .hero img{max-height:210px;margin:2rem auto 1.2rem}.home .hero h1{font-size:2rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.2rem auto}.home .hero .description{font-size:1.2rem}.home .hero .action-button{font-size:1rem;padding:.6rem 1.2rem}.home .feature h2{font-size:1.25rem}}.page{padding-top:var(--navbar-height);padding-left:var(--sidebar-width)}.navbar{position:fixed;z-index:20;top:0;left:0;right:0;height:var(--navbar-height);box-sizing:border-box;border-bottom:1px solid var(--c-border);background-color:var(--c-bg-navbar);transition:background-color var(--t-color),border-color var(--t-color)}.sidebar{font-size:16px;width:var(--sidebar-width);position:fixed;z-index:10;margin:0;top:var(--navbar-height);left:0;bottom:0;box-sizing:border-box;border-right:1px solid var(--c-border);overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--c-brand) var(--c-border);background-color:var(--c-bg-sidebar);transition:transform var(--t-transform),background-color var(--t-color),border-color var(--t-color)}.sidebar::-webkit-scrollbar{width:7px}.sidebar::-webkit-scrollbar-track{background-color:var(--c-border)}.sidebar::-webkit-scrollbar-thumb{background-color:var(--c-brand)}.sidebar-mask{position:fixed;z-index:9;top:0;left:0;width:100vw;height:100vh;display:none}.theme-container.sidebar-open .sidebar-mask{display:block}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1){transform:rotate(45deg) translate3d(5.5px,5.5px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(2){transform:scale3d(0,1,1)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform:rotate(-45deg) translate3d(6px,-6px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1),.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform-origin:center}.theme-container.no-navbar .theme-default-content h1,.theme-container.no-navbar .theme-default-content h2,.theme-container.no-navbar .theme-default-content h3,.theme-container.no-navbar .theme-default-content h4,.theme-container.no-navbar .theme-default-content h5,.theme-container.no-navbar .theme-default-content h6{margin-top:1.5rem;padding-top:0}.theme-container.no-navbar .page{padding-top:0}.theme-container.no-navbar .sidebar{top:0}.theme-container.no-sidebar .sidebar{display:none}@media (max-width: 719px){.theme-container.no-sidebar .sidebar{display:block}}.theme-container.no-sidebar .page{padding-left:0}.theme-default-content a:hover{text-decoration:underline}.theme-default-content img{max-width:100%}.theme-default-content h1,.theme-default-content h2,.theme-default-content h3,.theme-default-content h4,.theme-default-content h5,.theme-default-content h6{margin-top:calc(.5rem - var(--navbar-height));padding-top:calc(1rem + var(--navbar-height));margin-bottom:0}.theme-default-content h1:first-child,.theme-default-content h2:first-child,.theme-default-content h3:first-child,.theme-default-content h4:first-child,.theme-default-content h5:first-child,.theme-default-content h6:first-child{margin-bottom:1rem}.theme-default-content h1:first-child+p,.theme-default-content h1:first-child+pre,.theme-default-content h1:first-child+.custom-container,.theme-default-content h2:first-child+p,.theme-default-content h2:first-child+pre,.theme-default-content h2:first-child+.custom-container,.theme-default-content h3:first-child+p,.theme-default-content h3:first-child+pre,.theme-default-content h3:first-child+.custom-container,.theme-default-content h4:first-child+p,.theme-default-content h4:first-child+pre,.theme-default-content h4:first-child+.custom-container,.theme-default-content h5:first-child+p,.theme-default-content h5:first-child+pre,.theme-default-content h5:first-child+.custom-container,.theme-default-content h6:first-child+p,.theme-default-content h6:first-child+pre,.theme-default-content h6:first-child+.custom-container{margin-top:2rem}@media (max-width: 959px){.sidebar{font-size:15px;width:var(--sidebar-width-mobile)}.page{padding-left:var(--sidebar-width-mobile)}}@media (max-width: 719px){.sidebar{top:0;padding-top:var(--navbar-height);transform:translate(-100%)}.page{padding-left:0}.theme-container.sidebar-open .sidebar{transform:translate(0)}.theme-container.no-navbar .sidebar{padding-top:0}}@media (max-width: 419px){h1{font-size:1.9rem}}.navbar{--navbar-line-height: calc( var(--navbar-height) - 2 * var(--navbar-padding-v) );padding:var(--navbar-padding-v) var(--navbar-padding-h);line-height:var(--navbar-line-height)}.navbar .logo{height:var(--navbar-line-height);margin-right:var(--navbar-padding-v);vertical-align:top}.navbar .site-name{font-size:1.3rem;font-weight:600;color:var(--c-text);position:relative}.navbar .navbar-items-wrapper{display:flex;position:absolute;box-sizing:border-box;top:var(--navbar-padding-v);right:var(--navbar-padding-h);height:var(--navbar-line-height);padding-left:var(--navbar-padding-h);white-space:nowrap;font-size:.9rem}.navbar .navbar-items-wrapper .search-box{flex:0 0 auto;vertical-align:top}@media screen and (max-width: 719px){.navbar{padding-left:4rem}.navbar .site-name{display:block;width:calc(100vw - 11rem);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.navbar .can-hide{display:none}}.navbar-items{display:inline-block}@media print{.navbar-items{display:none}}.navbar-items a{display:inline-block;line-height:1.4rem;color:inherit}.navbar-items a:hover,.navbar-items a.router-link-active{color:var(--c-text)}.navbar-items .navbar-item{position:relative;display:inline-block;margin-left:1.5rem;line-height:var(--navbar-line-height)}.navbar-items .navbar-item:first-child{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.router-link-active{margin-bottom:-2px;border-bottom:2px solid var(--c-text-accent)}@media (max-width: 719px){.navbar-items .navbar-item{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.router-link-active{margin-bottom:0;border-bottom:none}.navbar-items a:hover,.navbar-items a.router-link-active{color:var(--c-text-accent)}}.toggle-sidebar-button{position:absolute;top:.6rem;left:1rem;display:none;padding:.6rem;cursor:pointer}.toggle-sidebar-button .icon{display:flex;flex-direction:column;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;cursor:inherit}.toggle-sidebar-button .icon span{display:inline-block;width:100%;height:2px;border-radius:2px;background-color:var(--c-text);transition:transform var(--t-transform)}.toggle-sidebar-button .icon span:nth-child(2){margin:6px 0}@media screen and (max-width: 719px){.toggle-sidebar-button{display:block}}.toggle-color-mode-button{display:flex;margin:auto;margin-left:1rem;border:0;background:none;color:var(--c-text);opacity:.8;cursor:pointer}@media print{.toggle-color-mode-button{display:none}}.toggle-color-mode-button:hover{opacity:1}.toggle-color-mode-button .icon{width:1.25rem;height:1.25rem}.DocSearch{transition:background-color var(--t-color)}.navbar-dropdown-wrapper{cursor:pointer}.navbar-dropdown-wrapper .navbar-dropdown-title,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:block;font-size:.9rem;font-family:inherit;cursor:inherit;padding:inherit;line-height:1.4rem;background:transparent;border:none;font-weight:500;color:var(--c-text)}.navbar-dropdown-wrapper .navbar-dropdown-title:hover,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{border-color:transparent}.navbar-dropdown-wrapper .navbar-dropdown-title .arrow,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile .arrow{vertical-align:middle;margin-top:-1px;margin-left:.4rem}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:none;font-weight:600;font-size:inherit}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item{color:inherit;line-height:1.7rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{margin:.45rem 0 0;border-top:1px solid var(--c-border);padding:1rem 0 .45rem;font-size:.9rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>span{padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a{font-weight:inherit}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a.router-link-active:after{display:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper{padding:0;list-style:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper .navbar-dropdown-subitem{font-size:.9em}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a{display:block;line-height:1.7rem;position:relative;border-bottom:none;font-weight:400;margin-bottom:0;padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a:hover,.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active:after{content:"";width:0;height:0;border-left:5px solid var(--c-text-accent);border-top:3px solid transparent;border-bottom:3px solid transparent;position:absolute;top:calc(50% - 2px);left:9px}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item:first-child .navbar-dropdown-subtitle{margin-top:0;padding-top:0;border-top:0}.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title-mobile{margin-bottom:.5rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:none}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:block}.navbar-dropdown-wrapper.mobile .navbar-dropdown{transition:height .1s ease-out;overflow:hidden}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{border-top:0;margin-top:0;padding-top:0;padding-bottom:0}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle,.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item>a{font-size:15px;line-height:2rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem{font-size:14px;padding-left:1rem}.navbar-dropdown-wrapper:not(.mobile){height:1.8rem}.navbar-dropdown-wrapper:not(.mobile):hover .navbar-dropdown,.navbar-dropdown-wrapper:not(.mobile).open .navbar-dropdown{display:block!important}.navbar-dropdown-wrapper:not(.mobile).open:blur{display:none}.navbar-dropdown-wrapper:not(.mobile) .navbar-dropdown{display:none;height:auto!important;box-sizing:border-box;max-height:calc(100vh - 2.7rem);overflow-y:auto;position:absolute;top:100%;right:0;background-color:var(--c-bg-navbar);padding:.6rem 0;border:1px solid var(--c-border);border-bottom-color:var(--c-border-dark);text-align:left;border-radius:.25rem;white-space:nowrap;margin:0}.page{padding-bottom:2rem;display:block}.page .theme-default-content{max-width:var(--content-width);margin:0 auto;padding:2rem 2.5rem;padding-top:0}@media (max-width: 959px){.page .theme-default-content{padding:2rem}}@media (max-width: 419px){.page .theme-default-content{padding:1.5rem}}.page-meta{max-width:var(--content-width);margin:0 auto;padding:1rem 2.5rem;overflow:auto}@media (max-width: 959px){.page-meta{padding:2rem}}@media (max-width: 419px){.page-meta{padding:1.5rem}}.page-meta .meta-item{cursor:default;margin-top:.8rem}.page-meta .meta-item .meta-item-label{font-weight:500;color:var(--c-text-lighter)}.page-meta .meta-item .meta-item-info{font-weight:400;color:var(--c-text-quote)}.page-meta .edit-link{display:inline-block;margin-right:.25rem}@media print{.page-meta .edit-link{display:none}}.page-meta .last-updated{float:right}@media (max-width: 719px){.page-meta .last-updated{font-size:.8em;float:none}.page-meta .contributors{font-size:.8em}}.page-nav{max-width:var(--content-width);margin:0 auto;padding:1rem 2.5rem 2rem;padding-bottom:0}@media (max-width: 959px){.page-nav{padding:2rem}}@media (max-width: 419px){.page-nav{padding:1.5rem}}.page-nav .inner{min-height:2rem;margin-top:0;border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding-top:1rem;overflow:auto}.page-nav .prev a:before{content:"←"}.page-nav .next{float:right}.page-nav .next a:after{content:"→"}.sidebar ul{padding:0;margin:0;list-style-type:none}.sidebar a{display:inline-block}.sidebar .navbar-items{display:none;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color);padding:.5rem 0 .75rem}.sidebar .navbar-items a{font-weight:600}.sidebar .navbar-items .navbar-item{display:block;line-height:1.25rem;font-size:1.1em;padding:.5rem 0 .5rem 1.5rem}.sidebar .sidebar-items{padding:1.5rem 0}@media (max-width: 719px){.sidebar .navbar-items{display:block}.sidebar .navbar-items .navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active:after{top:calc(1rem - 2px)}.sidebar .sidebar-items{padding:1rem 0}}.sidebar-item{cursor:default;border-left:.25rem solid transparent;color:var(--c-text)}.sidebar-item:focus-visible{outline-width:1px;outline-offset:-1px}.sidebar-item.active:not(p.sidebar-heading){font-weight:600;color:var(--c-text-accent);border-left-color:var(--c-text-accent)}.sidebar-item.sidebar-heading{transition:color .15s ease;font-size:1.1em;font-weight:700;padding:.35rem 1.5rem .35rem 1.25rem;width:100%;box-sizing:border-box;margin:0}.sidebar-item.sidebar-heading+.sidebar-item-children{transition:height .1s ease-out;overflow:hidden;margin-bottom:.75rem}.sidebar-item.collapsible{cursor:pointer}.sidebar-item.collapsible .arrow{position:relative;top:-.12em;left:.5em}.sidebar-item:not(.sidebar-heading){font-size:1em;font-weight:400;display:inline-block;margin:0;padding:.35rem 1rem .35rem 2rem;line-height:1.4;width:100%;box-sizing:border-box}.sidebar-item:not(.sidebar-heading)+.sidebar-item-children{padding-left:1rem;font-size:.95em}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading){padding:.25rem 1rem .25rem 1.75rem}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading).active{font-weight:500;border-left-color:transparent}a.sidebar-heading+.sidebar-item-children .sidebar-item:not(.sidebar-heading).active{border-left-color:transparent}a.sidebar-item{cursor:pointer}a.sidebar-item:hover{color:var(--c-text-accent)}.table-of-contents .badge{vertical-align:middle}.dropdown-enter-from,.dropdown-leave-to{height:0!important}.fade-slide-y-enter-active{transition:all .2s ease}.fade-slide-y-leave-active{transition:all .2s cubic-bezier(1,.5,.8,1)}.fade-slide-y-enter-from,.fade-slide-y-leave-to{transform:translateY(10px);opacity:0}:root{--c-brand: #a502ce;--c-brand-light: #b55ecb;--c-shadow: rgba(0,0,0,.2)}html.dark{--c-brand: #b55ecb;--c-brand-light: #a502ce;--c-shadow: rgba(255,255,255,.2)}a.external-link>svg~span:nth-child(2){display:none}.vertical-divider{display:inline-flex;width:0;border:solid;border-width:0 thin 0 0;border-color:var(--c-border);min-height:80%;vertical-align:text-bottom;margin:0 .5rem}.hero-logo{margin:3rem auto 1.5rem;display:flex;align-items:center;justify-content:center}.hero-logo span{font-weight:500;font-size:3rem;color:var(--c-text-accent);margin:0 0 0 .7rem}.grid{display:flex}.grid-logo{display:flex;align-items:center;width:25%;height:100px;margin-right:10px;padding:10px;border:1px solid var(--c-border);border-radius:5px;box-shadow:0 4px 8px 0 var(--c-shadow)}.grid-logo a{width:100%}.grid-logo img{width:100%;-webkit-filter:drop-shadow(1px 0 0 white) drop-shadow(0 1px 0 white) drop-shadow(-1px 0 0 white) drop-shadow(0 -1px 0 white);-filter:drop-shadow(1px 0 0 white) drop-shadow(0 1px 0 white) drop-shadow(-1px 0 0 white) drop-shadow(0 -1px 0 white)}:root{--search-bg-color: #ffffff;--search-accent-color: #3eaf7c;--search-text-color: #2c3e50;--search-border-color: #eaecef;--search-item-text-color: #5d81a5;--search-item-focus-bg-color: #f3f4f5;--search-input-width: 8rem;--search-result-width: 20rem}.search-box{display:inline-block;position:relative;margin-left:1rem}@media print{.search-box{display:none}}.search-box input{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:text;width:var(--search-input-width);height:2rem;color:var(--search-text-color);display:inline-block;border:1px solid var(--search-border-color);border-radius:2rem;font-size:.9rem;line-height:2rem;padding:0 .5rem 0 2rem;outline:none;transition:all ease .3s;background:var(--search-bg-color) url(/jmeter-dotnet-dsl/assets/search-0782d0d1.svg) .6rem .5rem no-repeat;background-size:1rem}.search-box input:focus{cursor:auto;border-color:var(--search-accent-color)}.search-box .suggestions{background:var(--search-bg-color);width:var(--search-result-width);position:absolute;top:2rem;right:0;border:1px solid var(--search-border-color);border-radius:6px;padding:.4rem;list-style-type:none}.search-box .suggestion{line-height:1.4;padding:.4rem .6rem;border-radius:4px;cursor:pointer}.search-box .suggestion.focus{background-color:var(--search-item-focus-bg-color)}.search-box .suggestion.focus a{color:var(--search-accent-color)}.search-box .suggestion a{white-space:normal;color:var(--search-item-text-color)}.search-box .suggestion .page-title{font-weight:600}.search-box .suggestion .page-header{font-size:.9em;margin-left:.25em}@media (max-width: 719px){.search-box input{cursor:pointer;width:0;border-color:transparent;position:relative}.search-box input:focus{cursor:text;left:0;width:10rem}}@media (max-width: 419px){.search-box input:focus{width:8rem}.search-box .suggestions{width:calc(100vw - 4rem);right:-.5rem}}html.dark{--box-shadow: #0f0e0d;--card-shadow: rgba(0, 0, 0, .3);--black: #fff;--dark-grey: #999;--light-grey: #666;--white: #000;--grey3: #bbb;--grey12: #333;--grey14: #111}:root{--vp-bg: var(--c-bg, #fff);--vp-bgl: var(--c-bg-light, #f3f4f5);--vp-bglt: var(--c-bg-lighter, #eeeeee);--vp-c: var(--c-text, #2c3e50);--vp-cl: var(--c-text-light, #3a5169);--vp-clt: var(--c-text-lighter, #4e6e8e);--vp-brc: var(--c-border, #eaecef);--vp-brcd: var(--c-border-dark, #dfe2e5);--vp-tc: var(--c-brand, #3eaf7c);--vp-tcl: var(--c-brand-light, #4abf8a);--vp-ct: var(--t-color, .3s ease);--vp-tt: var(--t-transform, .3s ease);--box-shadow: #f0f1f2;--card-shadow: rgba(0, 0, 0, .15);--black: #000;--dark-grey: #666;--light-grey: #999;--white: #fff;--grey3: #333;--grey12: #bbb;--grey14: #eee}:root{--balloon-border-radius: 2px;--balloon-color: rgba(16, 16, 16, .95);--balloon-text-color: #fff;--balloon-font-size: 12px;--balloon-move: 4px}button[aria-label][data-balloon-pos]{overflow:visible}[aria-label][data-balloon-pos]{position:relative;cursor:pointer}[aria-label][data-balloon-pos]:after{opacity:0;pointer-events:none;transition:all .18s ease-out .18s;text-indent:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-weight:400;font-style:normal;text-shadow:none;font-size:var(--balloon-font-size);background:var(--balloon-color);border-radius:2px;color:var(--balloon-text-color);border-radius:var(--balloon-border-radius);content:attr(aria-label);padding:.5em 1em;position:absolute;white-space:nowrap;z-index:10}[aria-label][data-balloon-pos]:before{width:0;height:0;border:5px solid transparent;border-top-color:var(--balloon-color);opacity:0;pointer-events:none;transition:all .18s ease-out .18s;content:"";position:absolute;z-index:10}[aria-label][data-balloon-pos]:hover:before,[aria-label][data-balloon-pos]:hover:after,[aria-label][data-balloon-pos][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-visible]:after,[aria-label][data-balloon-pos]:not([data-balloon-nofocus]):focus:before,[aria-label][data-balloon-pos]:not([data-balloon-nofocus]):focus:after{opacity:1;pointer-events:none}[aria-label][data-balloon-pos].font-awesome:after{font-family:FontAwesome,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif}[aria-label][data-balloon-pos][data-balloon-break]:after{white-space:pre}[aria-label][data-balloon-pos][data-balloon-break][data-balloon-length]:after{white-space:pre-line;word-break:break-word}[aria-label][data-balloon-pos][data-balloon-blunt]:before,[aria-label][data-balloon-pos][data-balloon-blunt]:after{transition:none}[aria-label][data-balloon-pos][data-balloon-pos=up]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=up][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos=down]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=down][data-balloon-visible]:after{transform:translate(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=up]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=up][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos=down]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=down][data-balloon-visible]:before{transform:translate(-50%)}[aria-label][data-balloon-pos][data-balloon-pos*=-left]:after{left:0}[aria-label][data-balloon-pos][data-balloon-pos*=-left]:before{left:5px}[aria-label][data-balloon-pos][data-balloon-pos*=-right]:after{right:0}[aria-label][data-balloon-pos][data-balloon-pos*=-right]:before{right:5px}[aria-label][data-balloon-pos][data-balloon-po*=-left]:hover:after,[aria-label][data-balloon-pos][data-balloon-po*=-left][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos*=-right]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos*=-right][data-balloon-visible]:after{transform:translate(0)}[aria-label][data-balloon-pos][data-balloon-po*=-left]:hover:before,[aria-label][data-balloon-pos][data-balloon-po*=-left][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos*=-right]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos*=-right][data-balloon-visible]:before{transform:translate(0)}[aria-label][data-balloon-pos][data-balloon-pos^=up]:before,[aria-label][data-balloon-pos][data-balloon-pos^=up]:after{bottom:100%;transform-origin:top;transform:translateY(var(--balloon-move))}[aria-label][data-balloon-pos][data-balloon-pos^=up]:after{margin-bottom:10px}[aria-label][data-balloon-pos][data-balloon-pos=up]:before,[aria-label][data-balloon-pos][data-balloon-pos=up]:after{left:50%;transform:translate(-50%,var(--balloon-move))}[aria-label][data-balloon-pos][data-balloon-pos^=down]:before,[aria-label][data-balloon-pos][data-balloon-pos^=down]:after{top:100%;transform:translateY(calc(var(--balloon-move) * -1))}[aria-label][data-balloon-pos][data-balloon-pos^=down]:after{margin-top:10px}[aria-label][data-balloon-pos][data-balloon-pos^=down]:before{width:0;height:0;border:5px solid transparent;border-bottom-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-pos=down]:after,[aria-label][data-balloon-pos][data-balloon-pos=down]:before{left:50%;transform:translate(-50%,calc(var(--balloon-move) * -1))}[aria-label][data-balloon-pos][data-balloon-pos=left]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=left][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos=right]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=right][data-balloon-visible]:after{transform:translateY(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=left][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos=right]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=right][data-balloon-visible]:before{transform:translateY(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:after,[aria-label][data-balloon-pos][data-balloon-pos=left]:before{right:100%;top:50%;transform:translate(var(--balloon-move),-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:after{margin-right:10px}[aria-label][data-balloon-pos][data-balloon-pos=left]:before{width:0;height:0;border:5px solid transparent;border-left-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-pos=right]:after,[aria-label][data-balloon-pos][data-balloon-pos=right]:before{left:100%;top:50%;transform:translate(calc(var(--balloon-move) * -1),-50%)}[aria-label][data-balloon-pos][data-balloon-pos=right]:after{margin-left:10px}[aria-label][data-balloon-pos][data-balloon-pos=right]:before{width:0;height:0;border:5px solid transparent;border-right-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-length]:after{white-space:normal}[aria-label][data-balloon-pos][data-balloon-length=small]:after{width:80px}[aria-label][data-balloon-pos][data-balloon-length=medium]:after{width:150px}[aria-label][data-balloon-pos][data-balloon-length=large]:after{width:260px}[aria-label][data-balloon-pos][data-balloon-length=xlarge]:after{width:380px}@media screen and (max-width: 768px){[aria-label][data-balloon-pos][data-balloon-length=xlarge]:after{width:90vw}}[aria-label][data-balloon-pos][data-balloon-length=fit]:after{width:100%}:root{--copy-icon: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");--copied-icon: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E")}div[class*=language-]>button.copy-code-button{border-width:0;background:transparent;position:absolute;outline:none;cursor:pointer}@media print{div[class*=language-]>button.copy-code-button{display:none}}div[class*=language-]>button.copy-code-button .copy-icon{background:currentcolor;-webkit-mask-image:var(--copy-icon);mask-image:var(--copy-icon);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:1em;mask-size:1em}div[class*=language-]>button.copy-code-button:not(.fancy){border-width:0;background:transparent;cursor:pointer;position:absolute;top:.5em;right:.5em;z-index:5;width:2.5rem;height:2.5rem;padding:0;border-radius:.5rem;opacity:0;transition:opacity .4s}div[class*=language-]>button.copy-code-button:not(.fancy):hover,div[class*=language-]>button.copy-code-button:not(.fancy).copied{background:var(--code-hl-bg-color, rgba(0, 0, 0, .66))}div[class*=language-]>button.copy-code-button:not(.fancy):focus,div[class*=language-]>button.copy-code-button:not(.fancy).copied{opacity:1}div[class*=language-]>button.copy-code-button:not(.fancy).copied:after{content:attr(data-copied);position:absolute;top:0;right:calc(100% + .25rem);display:block;height:1.25rem;padding:.625rem;border-radius:.5rem;background:var(--code-hl-bg-color, rgba(0, 0, 0, .66));color:var(--code-ln-color, #9e9e9e);font-weight:500;line-height:1.25rem;white-space:nowrap}div[class*=language-]>button.copy-code-button:not(.fancy) .copy-icon{width:1.25rem;height:1.25rem;padding:.625rem;color:var(--code-ln-color, #9e9e9e);font-size:1.25rem}div[class*=language-]>button.copy-code-button.fancy{right:-14px;bottom:-14px;z-index:5;width:2rem;height:2rem;padding:7px 8px;border-radius:50%;background:#339af0;color:#fff}@media (max-width: 419px){div[class*=language-]>button.copy-code-button.fancy{right:0;bottom:0;width:28px;height:28px;border-radius:50% 10% 0}}div[class*=language-]>button.copy-code-button.fancy:hover{background:#228be6}div[class*=language-]>button.copy-code-button.fancy .copy-icon{width:100%;height:100%;color:#fff;font-size:1.25rem}@media (max-width: 419px){div[class*=language-]>button.copy-code-button.fancy .copy-icon{position:relative;top:2px;left:2px}}div[class*=language-]>button.copy-code-button.copied .copy-icon{-webkit-mask-image:var(--copied-icon);mask-image:var(--copied-icon)}div[class*=language-]:hover:before{display:none}div[class*=language-]:hover>button.copy-code-button:not(.fancy){opacity:1}:root{--medium-zoom-z-index: 100;--medium-zoom-bg-color: #ffffff;--medium-zoom-opacity: 1}.medium-zoom-overlay{background-color:var(--medium-zoom-bg-color)!important;z-index:var(--medium-zoom-z-index)}.medium-zoom-overlay~img{z-index:calc(var(--medium-zoom-z-index) + 1)}.medium-zoom--opened .medium-zoom-overlay{opacity:var(--medium-zoom-opacity)}
diff --git a/assets/test-plan-gui-a4e8b653.png b/assets/test-plan-gui-a4e8b653.png
new file mode 100644
index 0000000..5de4198
Binary files /dev/null and b/assets/test-plan-gui-a4e8b653.png differ
diff --git a/assets/ultimate-thread-group-gui-400bcb90.png b/assets/ultimate-thread-group-gui-400bcb90.png
new file mode 100644
index 0000000..8703fb1
Binary files /dev/null and b/assets/ultimate-thread-group-gui-400bcb90.png differ
diff --git a/assets/ultimate-thread-group-timeline-471befb4.png b/assets/ultimate-thread-group-timeline-471befb4.png
new file mode 100644
index 0000000..71073cc
Binary files /dev/null and b/assets/ultimate-thread-group-timeline-471befb4.png differ
diff --git a/assets/view-results-tree-431a3001.png b/assets/view-results-tree-431a3001.png
new file mode 100644
index 0000000..77bcf22
Binary files /dev/null and b/assets/view-results-tree-431a3001.png differ
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..75de640
Binary files /dev/null and b/favicon.ico differ
diff --git a/guide/index.html b/guide/index.html
new file mode 100644
index 0000000..33ed165
--- /dev/null
+++ b/guide/index.html
@@ -0,0 +1,403 @@
+
+
+
+
+
+
+
+
+
.java User guide
Setup
dotnet add package Abstracta.JmeterDsl --version 0.5
+
Simple HTTP test plan
HttpSampler
.http://my.service
.using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ ),
+ //this is just to log details of each request stats
+ JtlWriter("jtls")
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
HttpSampler("home", "http://my.service")
) to easily check their respective statistics.EmbeddedJMeterEngine
and the JvmArgs
method like in the following example:using Abstracta.JmeterDsl.Core.Engines;
+...
+var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+).RunIn(new EmbeddedJmeterEngine()
+ .JvmArgs("-Xmx4g")
+);
+
dotnet test -v n
and add the following code to your tests:private TextWriter? originalConsoleOut;
+
+// Redirecting output to progress to get live stdout with nunit.
+// https://github.com/nunit/nunit3-vs-adapter/issues/343
+// https://github.com/nunit/nunit/issues/1139
+[SetUp]
+public void SetUp()
+{
+ originalConsoleOut = Console.Out;
+ Console.SetOut(TestContext.Progress);
+}
+
+[TearDown]
+public void TearDown()
+{
+ Console.SetOut(originalConsoleOut!);
+}
+
Run test at scale
Azure Load Testing
dotnet add package Abstracta.JmeterDsl.Azure --version 0.5
+
using Abstracta.JmeterDsl.Azure;
+using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+ ).RunIn(new AzureEngine(Environment.GetEnvironmentVariable("AZURE_CREDS")) // AZURE_CREDS=tenantId:clientId:secretId
+ .TestName("dsl-test")
+ /*
+ This specifies the number of engine instances used to execute the test plan.
+ In this case, means that it will run 2(threads in thread group)x2(engines)=4 concurrent users/threads in total.
+ Each engine executes the test plan independently.
+ */
+ .Engines(2)
+ .TestTimeout(TimeSpan.FromMinutes(20)));
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
AZURE_CREDS
, a custom environment variable containing tenantId:clientId:clientSecret
with proper values for each. Check at Azure Portal tenant properties the proper tenant ID for your subscription, and follow this guide to register an application with proper permissions and secrets generation for tests execution.log4j2.xml
configuration file:<Logger name="us.abstracta.jmeter.javadsl.azure.AzureClient" level="DEBUG"/>
+<Logger name="okhttp3" level="DEBUG"/>
+
BlazeMeter
dotnet add package Abstracta.JmeterDsl.BlazeMeter --version 0.5
+
using Abstracta.JmeterDsl.BlazeMeter;
+using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ // number of threads and iterations are in the end overwritten by BlazeMeter engine settings
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+ ).RunIn(new BlazeMeterEngine(Environment.GetEnvironmentVariable("BZ_TOKEN"))
+ .TestName("DSL test")
+ .TotalUsers(500)
+ .HoldFor(TimeSpan.FromMinutes(10))
+ .ThreadsPerEngine(100)
+ .TestTimeout(TimeSpan.FromMinutes(20))
+ .TestName("dsl-test"));
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
BZ_TOKEN
, a custom environment variable with <KEY_ID>:<KEY_SECRET>
format, to get the BlazeMeter API authentication credentials..RunIn(new BlazeMeterEngine(...))
to any existing JMeter DSL test to get it running at scale in BlazeMeter.BlazeMeterEngine
always returns 0 as sentBytes
statistics since there is no efficient way to get it from BlazMeter.log4j2.xml
configuration file:<Logger name="us.abstracta.jmeter.javadsl.blazemeter.BlazeMeterClient" level="DEBUG"/>
+<Logger name="okhttp3" level="DEBUG"/>
+
Advanced threads configuration
ThreadGroup(10, 20, ...) // 10 threads for 20 iterations each
+ThreadGroup(10, TimeSpan.FromSeconds(20), ...) // 10 threads for 20 seconds each
+
Thread ramps and holds
ThreadGroup().RampTo(10, TimeSpan.FromSeconds(5)).HoldIterating(20) // ramp to 10 threads for 5 seconds (1 thread every half second) and iterating each thread 20 times
+ThreadGroup().RampToAndHold(10, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(20)) //similar as above but after ramping up holding execution for 20 seconds
+
ThreadGroup()
+ .RampToAndHold(10, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(20))
+ .RampToAndHold(100, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30))
+ .RampTo(200, TimeSpan.FromSeconds(10))
+ .RampToAndHold(100, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30))
+ .RampTo(0, TimeSpan.FromSeconds(5))
+ .Children(
+ HttpSampler("http://my.service")
+ )
+
TestElement.ShowInGui()
method, which shows the JMeter test element GUI that could help you understand what will DSL execute in JMeter. You can use this method with any test element generated by the DSL (not just thread groups).ThreadGroup("main", 1, 1, ...)
) on them to properly identify associated requests in statistics & jtl results. Test plan debugging
View results tree
ResultsTreeVisualizer()
like in the following example:using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ ),
+ ResultsTreeVisualizer()
+ ).Run();
+ }
+}
+
ResultsTreeVisualizer()
as a child of a thread group, it will only display sample results of that thread group. When added as a child of a sampler, it will only show sample results for that sampler. You can use this to only review certain sample results in your test plan.ResultsTreeVisualizer()
from test plans when are no longer needed (when debugging is finished). Leaving them might interfere with unattended test plan execution (eg: in CI) due to test plan execution not finishing until all visualizers windows are closed.ResultsLimit(int)
method which allows changing this value. Take into consideration that the more results are shown, the more memory that will require. So use this setting with care. Debug JMeter code
EmbeddedJmeterEngine
like in the following example:TestPlan(
+ ThreadGroup(threads: 1, iterations: 1,
+ HttpSampler("http://my.service")
+ )
+ ).RunIn(new EmbeddedJmeterEngine()
+ .JvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"));
+
suspend
flag to y
to block test execution until Remote JVM Debug is run in IDE.org.apache.jmeter.threads.JMeterThread
. You can check the classes used by each DSL-provided test element by checking the Java DSL code. Dummy sampler
DummySampler
(which uses Dummy Sampler plugin) to be able to test extractors, assertions, controllers conditions, and other parts of the test plan under certain conditions/results generated by the samplers.using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ TestPlan(
+ ThreadGroup(2, 10,
+ // HttpSampler("http://my.service")
+ DummySampler("{\"status\" : \"OK\"}")
+ )
+ ).Run();
+ }
+}
+
ResponseTimeSimulation()
method. Test plan review un JMeter GUI
ShowInGui()
method in a test plan to open JMeter GUI with the preloaded test plan.using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+ ).ShowInGui();
+ }
+}
+
Reporting
Log requests and responses
Abstracta.JmeterDsl
) to get information about generated requests, responses, and associated metrics is through the generation of JTL files.JtlWriter
like in this example:using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ ),
+ JtlWriter("jtls")
+ ).Run();
+ }
+}
+
JtlWriter
will write the most used information to evaluate the performance of the tested service. If you want to trace all the information of each request you may use JtlWriter
with the WithAllFields()
option. Doing this will provide all the information at the cost of additional computation and resource usage (fewer resources for actual load testing). You can tune which fields to include or not with JtlWriter
and only log what you need, check JtlWriter for more details.JtlWriter
will automatically generate .jtl
files applying this format: <yyyy-MM-dd HH-mm-ss> <UUID>.jtl
.JtlWriter(directory, fileName)
.ResponseFileSaver
which automatically generates a file for each received response. Here is an example:using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ ),
+ ResponseFileSaver(DateTime.Now.ToString("dd-MM-yyyy HH:mm:ss").Replace(":", "-") + "-response")
+ ).Run();
+ }
+}
+
Response processing
Use part of a response in a subsequent request (aka correlation)
Regular expressions extraction
using System.Net.Http.Headers;
+using System.Net.Mime;
+
+using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service/accounts")
+ .Post("{\"name\": \"John Doe\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
+ .Children(
+ RegexExtractor("ACCOUNT_ID", "\"id\":\"([^\"]+)\"")
+ ),
+ HttpSampler("http://my.service/accounts/${ACCOUNT_ID}")
+ )
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
Requests generation
Loops
Iterating a fixed number of times
ForLoopController
(which uses JMeter Loop Controller component) as in the following example:using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(2, 10,
+ ForLoopController(5,
+ HttpSampler("http://my.service/accounts")
+ )
+ )
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
__jm__<loopName>__idx
with the current index of for loop iteration (starting with 0) which you can use in children elements. The default name for the for loop controller, when not specified, is for
. CSV as input data for requests
CsvDataSet
element. For example, having a file like this one:USER,PASS
+user1,pass1
+user2,pass2
+
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)));
+ }
+}
+
SharedIn
method like in the following example: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)));
+
RandomOrder()
method to get CSV lines in random order (using Random CSV Data Set plugin), but this is less performant as getting them sequentially, so use it sparingly. Provide request parameters programmatically per request
jsr223preProcessor
to specify your own logic to build each request.using System;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using static Abstracta.JmeterDsl.JmeterDsl;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var stats = TestPlan(
+ ThreadGroup(5, 10,
+ HttpSampler("http://my.service")
+ .Post("${REQUEST_BODY}", new MediaTypeHeaderValue(MediaTypeNames.Text.Plain))
+ .Children(Jsr223PreProcessor("vars.put('REQUEST_BODY', '{\"time\": \"' + Instant.now() + '\"}')"))
+ )
+ ).Run();
+ Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
+ }
+}
+
Groovy
is used, but you can change to others by using the provided Language()
method. Protocols
HTTP
Methods & body
HttpSampler("http://my.service") // A simple get
+HttpSampler("http://my.service")
+ .Post("{\"field\":\"val\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) // simple post
+
HttpSampler("http://my.service")
+ .Method(HttpMethod.Put.Method)
+ .ContentType(new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
+ .Body("{\"field\":\"val\"}")
+
Parameters
Param
method as in the following example:using static Abstracta.JmeterDsl.JmeterDsl;
+using System.Net.Http;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ var baseUrl = "https://myservice.com/products";
+ TestPlan(
+ ThreadGroup(1, 1,
+ // GET https://myservice.com/products?name=iron+chair
+ HttpSampler("GetIronChair", baseUrl)
+ .Param("name", "iron chair"),
+ /*
+ * POST https://myservice.com/products
+ * Content-Type: application/x-www-form-urlencoded
+ *
+ * name=wooden+chair
+ */
+ HttpSampler("CreateWoodenChair", baseUrl)
+ .Method(HttpMethod.Post.Method) // POST
+ .Param("name", "wooden chair")
+ )
+ ).Run();
+ }
+}
+
RawParam
method instead which does not apply any encoding to the parameter name or value, and send it as is. Headers
Content-Type
header is being set in two different ways:HttpSampler("http://my.service")
+ .Post("{\"field\":\"val\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
+HttpSampler("http://my.service")
+ .ContentType(new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
+
Content-Type
header, but you can also set any header on a particular request using provided Header
method, like this:HttpSampler("http://my.service")
+ .Header("X-First-Header", "val1")
+ .Header("X-Second-Header", "val2")
+
HttpHeaders
like this:TestPlan(
+ ThreadGroup(2, 10,
+ HttpHeaders()
+ .Header("X-Header", "val1"),
+ HttpSampler("http://my.service"),
+ HttpSampler("http://my.service/users")
+ )
+).Run();
+
Multipart requests
BodyPart
and BodyFilePart
methods like in the following example:using static Abstracta.JmeterDsl.JmeterDsl;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+
+public class PerformanceTest
+{
+ [Test]
+ public void LoadTest()
+ {
+ TestPlan(
+ ThreadGroup(1, 1,
+ HttpSampler("https://myservice.com/report"),
+ .Method(HttpMethod.Post.Method)
+ .BodyPart("myText", "Hello World", new MediaTypeHeaderValue(MediaTypeNames.Text.Plain))
+ .BodyFilePart("myFile", "myReport.xml", new MediaTypeHeaderValue(MediaTypeNames.Text.Xml))
+ )
+ ).Run();
+ }
+}
+
Cookies & caching
TestPlan(
+ HttpCookies().Disable(),
+ HttpCache().Disable(),
+ ThreadGroup(2, 10,
+ HttpSampler("http://my.service")
+ )
+)
+