diff --git a/404.html b/404.html new file mode 100644 index 00000000..c2fa62ae --- /dev/null +++ b/404.html @@ -0,0 +1,38 @@ + + + + + + + + + jmeter-java-dsl + + + + +

404

How did we get here?
Take me home
+ + + diff --git a/assets/404.html-60b35caa.js b/assets/404.html-60b35caa.js new file mode 100644 index 00000000..7a25b17a --- /dev/null +++ b/assets/404.html-60b35caa.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-3706649a","path":"/404.html","title":"","lang":"en-US","frontmatter":{"layout":"NotFound"},"headers":[],"git":{},"filePathRelative":null}');export{t as data}; diff --git a/assets/404.html-c29c87b1.js b/assets/404.html-c29c87b1.js new file mode 100644 index 00000000..0430fb7c --- /dev/null +++ b/assets/404.html-c29c87b1.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app-863e42f0.js";const _={};function o(r,n){return c(),t("div")}const a=e(_,[["render",o],["__file","404.html.vue"]]);export{a as default}; diff --git a/assets/Carousel-bd2695ae.js b/assets/Carousel-bd2695ae.js new file mode 100644 index 00000000..774c2466 --- /dev/null +++ b/assets/Carousel-bd2695ae.js @@ -0,0 +1 @@ +import{f as w,g as c,r as b,o as k,h as x,w as X,a as l,n as y,i as S,u,d as _,C as D,_ as B}from"./app-863e42f0.js";const W={class:"carousel-track"},z=["disabled"],M=["disabled"],p=(400+10*2)*2,N=w({__name:"Carousel",setup(R){const o=c(null),a=c(!1);let t=c(0),i=0;const d=e=>{e.preventDefault(),i=v(e),a.value=!0,t.value=o.value.scrollLeft,document.addEventListener("mousemove",n),document.addEventListener("touchmove",n),document.addEventListener("mouseup",r),document.addEventListener("touchend",r)},v=e=>h(e)?e.pageX:e.touches[0].pageX,h=e=>e.type.startsWith("mouse"),n=e=>{const s=v(e)-i;o.value.scrollLeft=t.value-s},r=()=>{document.removeEventListener("mousemove",n),document.removeEventListener("touchmove",n),document.removeEventListener("mouseup",r),document.removeEventListener("touchend",r),t.value=o.value.scrollLeft,a.value=!1},g=()=>f(-p),f=e=>o.value.scroll({top:0,left:o.value.scrollLeft+e,behavior:"smooth"}),L=()=>f(p),C=e=>{if(!o.value)return!0;let s=o.value.scrollWidth-o.value.offsetWidth;return s===0?!0:e{a.value||(t.value=o.value.scrollLeft)};return(e,s)=>{const m=b("font-awesome-icon");return k(),x(u(D),null,{default:X(()=>[l("section",{class:y(["carousel",a.value&&"is-dragging"])},[l("div",{class:"carousel-viewport",ref_key:"viewport",ref:o,onMousedown:d,onTouchstart:d,onScroll:E},[l("div",W,[S(e.$slots,"default")])],544),l("button",{onClick:g,class:"carousel-control carousel-prev",disabled:u(t)<=0},[_(m,{icon:"fa-solid fa-chevron-left"})],8,z),l("button",{onClick:L,class:"carousel-control carousel-next",disabled:!C(u(t))},[_(m,{icon:"fa-solid fa-chevron-right"})],8,M)],2)]),_:3})}}});const O=B(N,[["__file","Carousel.vue"]]);export{O as default}; diff --git a/assets/Testimonial-b9405fa9.js b/assets/Testimonial-b9405fa9.js new file mode 100644 index 00000000..3db43570 --- /dev/null +++ b/assets/Testimonial-b9405fa9.js @@ -0,0 +1 @@ +import{f as m,g as d,t as f,j as p,r as n,o as v,c as k,a as o,d as i,u as s,k as b,i as g,_ as h}from"./app-863e42f0.js";const y={class:"source"},w={class:"position"},x={class:"comment"},T=m({__name:"Testimonial",props:{item:{type:Object,required:!0}},setup(a){const c=d(null),r=a,{item:t}=f(r),l=p(()=>{let e=t.value.source;return e.includes("twitter.com")?"fa-brands fa-twitter":e.includes("linkedin.com")?"fa-brands fa-linkedin":e.includes("github.com")?"fa-brands fa-github":"fa-solid fa-globe"});return(e,B)=>{const u=n("font-awesome-icon"),_=n("AutoLink");return v(),k("div",{class:"testimonial",ref_key:"root",ref:c},[o("div",y,[o("span",null,[i(u,{icon:l.value},null,8,["icon"]),i(_,{item:{link:s(t).source,text:s(t).name}},null,8,["item"])])]),o("div",w,b(s(t).position),1),o("div",x,[g(e.$slots,"default")])],512)}}});const A=h(T,[["__file","Testimonial.vue"]]);export{A as default}; diff --git a/assets/abstracta-logo-63bce99b.png b/assets/abstracta-logo-63bce99b.png new file mode 100644 index 00000000..ea946ec4 Binary files /dev/null and b/assets/abstracta-logo-63bce99b.png differ diff --git a/assets/app-863e42f0.js b/assets/app-863e42f0.js new file mode 100644 index 00000000..30ab1a33 --- /dev/null +++ b/assets/app-863e42f0.js @@ -0,0 +1,770 @@ +const Fu="modulepreload",ju=function(e){return"/jmeter-java-dsl/"+e},$i={},He=function(t,n,r){if(!n||n.length===0)return t();const a=document.getElementsByTagName("link");return Promise.all(n.map(o=>{if(o=ju(o),o in $i)return;$i[o]=!0;const i=o.endsWith(".css"),s=i?'[rel="stylesheet"]':"";if(!!r)for(let u=a.length-1;u>=0;u--){const f=a[u];if(f.href===o&&(!i||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${o}"]${s}`))return;const c=document.createElement("link");if(c.rel=i?"stylesheet":Fu,i||(c.as="script",c.crossOrigin=""),c.href=o,document.head.appendChild(c),i)return new Promise((u,f)=>{c.addEventListener("load",u),c.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${o}`)))})})).then(()=>t())};function Bo(e,t){const n=Object.create(null),r=e.split(",");for(let a=0;a!!n[a.toLowerCase()]:a=>!!n[a]}const xe={},In=[],ht=()=>{},Bu=()=>!1,Uu=/^on[^a-z]/,Er=e=>Uu.test(e),Uo=e=>e.startsWith("onUpdate:"),Ne=Object.assign,qo=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},qu=Object.prototype.hasOwnProperty,pe=(e,t)=>qu.call(e,t),te=Array.isArray,Rn=e=>Ta(e)==="[object Map]",El=e=>Ta(e)==="[object Set]",ie=e=>typeof e=="function",ge=e=>typeof e=="string",Aa=e=>typeof e=="symbol",Se=e=>e!==null&&typeof e=="object",Cl=e=>(Se(e)||ie(e))&&ie(e.then)&&ie(e.catch),Sl=Object.prototype.toString,Ta=e=>Sl.call(e),Wu=e=>Ta(e).slice(8,-1),Al=e=>Ta(e)==="[object Object]",Wo=e=>ge(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ar=Bo(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),La=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Ku=/-(\w)/g,Et=La(e=>e.replace(Ku,(t,n)=>n?n.toUpperCase():"")),Vu=/\B([A-Z])/g,kn=La(e=>e.replace(Vu,"-$1").toLowerCase()),Pa=La(e=>e.charAt(0).toUpperCase()+e.slice(1)),Ka=La(e=>e?`on${Pa(e)}`:""),yn=(e,t)=>!Object.is(e,t),Va=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},Yu=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Gu=e=>{const t=ge(e)?Number(e):NaN;return isNaN(t)?e:t};let zi;const po=()=>zi||(zi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Cr(e){if(te(e)){const t={};for(let n=0;n{if(n){const r=n.split(Ju);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function Qe(e){let t="";if(ge(e))t=e;else if(te(e))for(let n=0;nge(e)?e:e==null?"":te(e)||Se(e)&&(e.toString===Sl||!ie(e.toString))?JSON.stringify(e,Ll,2):String(e),Ll=(e,t)=>t&&t.__v_isRef?Ll(e,t.value):Rn(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,a])=>(n[`${r} =>`]=a,n),{})}:El(t)?{[`Set(${t.size})`]:[...t.values()]}:Se(t)&&!te(t)&&!Al(t)?String(t):t;let tt;class nf{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=tt,!t&&tt&&(this.index=(tt.scopes||(tt.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=tt;try{return tt=this,t()}finally{tt=n}}}on(){tt=this}off(){tt=this.parent}stop(t){if(this._active){let n,r;for(n=0,r=this.effects.length;n{const t=new Set(e);return t.w=0,t.n=0,t},Ol=e=>(e.w&Qt)>0,Il=e=>(e.n&Qt)>0,of=({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let r=0;r{(u==="length"||!Aa(u)&&u>=l)&&s.push(c)})}else switch(n!==void 0&&s.push(i.get(n)),t){case"add":te(e)?Wo(n)&&s.push(i.get("length")):(s.push(i.get(vn)),Rn(e)&&s.push(i.get(vo)));break;case"delete":te(e)||(s.push(i.get(vn)),Rn(e)&&s.push(i.get(vo)));break;case"set":Rn(e)&&s.push(i.get(vn));break}if(s.length===1)s[0]&&go(s[0]);else{const l=[];for(const c of s)c&&l.push(...c);go(Ko(l))}}function go(e,t){const n=te(e)?e:[...e];for(const r of n)r.computed&&Hi(r);for(const r of n)r.computed||Hi(r)}function Hi(e,t){(e!==dt||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}function lf(e,t){var n;return(n=ua.get(e))==null?void 0:n.get(t)}const cf=Bo("__proto__,__v_isRef,__isVue"),Ml=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Aa)),Fi=uf();function uf(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const r=he(this);for(let o=0,i=this.length;o{e[t]=function(...n){Wn();const r=he(this)[t].apply(this,n);return Kn(),r}}),e}function ff(e){const t=he(this);return Ze(t,"has",e),t.hasOwnProperty(e)}class $l{constructor(t=!1,n=!1){this._isReadonly=t,this._shallow=n}get(t,n,r){const a=this._isReadonly,o=this._shallow;if(n==="__v_isReactive")return!a;if(n==="__v_isReadonly")return a;if(n==="__v_isShallow")return o;if(n==="__v_raw"&&r===(a?o?Ef:Fl:o?Hl:Dl).get(t))return t;const i=te(t);if(!a){if(i&&pe(Fi,n))return Reflect.get(Fi,n,r);if(n==="hasOwnProperty")return ff}const s=Reflect.get(t,n,r);return(Aa(n)?Ml.has(n):cf(n))||(a||Ze(t,"get",n),o)?s:Fe(s)?i&&Wo(n)?s:s.value:Se(s)?a?Vn(s):Sr(s):s}}class zl extends $l{constructor(t=!1){super(!1,t)}set(t,n,r,a){let o=t[n];if(Dn(o)&&Fe(o)&&!Fe(r))return!1;if(!this._shallow&&(!fa(r)&&!Dn(r)&&(o=he(o),r=he(r)),!te(t)&&Fe(o)&&!Fe(r)))return o.value=r,!0;const i=te(t)&&Wo(n)?Number(n)e,Oa=e=>Reflect.getPrototypeOf(e);function $r(e,t,n=!1,r=!1){e=e.__v_raw;const a=he(e),o=he(t);n||(yn(t,o)&&Ze(a,"get",t),Ze(a,"get",o));const{has:i}=Oa(a),s=r?Yo:n?Jo:dr;if(i.call(a,t))return s(e.get(t));if(i.call(a,o))return s(e.get(o));e!==a&&e.get(t)}function zr(e,t=!1){const n=this.__v_raw,r=he(n),a=he(e);return t||(yn(e,a)&&Ze(r,"has",e),Ze(r,"has",a)),e===a?n.has(e):n.has(e)||n.has(a)}function Dr(e,t=!1){return e=e.__v_raw,!t&&Ze(he(e),"iterate",vn),Reflect.get(e,"size",e)}function ji(e){e=he(e);const t=he(this);return Oa(t).has.call(t,e)||(t.add(e),Ot(t,"add",e,e)),this}function Bi(e,t){t=he(t);const n=he(this),{has:r,get:a}=Oa(n);let o=r.call(n,e);o||(e=he(e),o=r.call(n,e));const i=a.call(n,e);return n.set(e,t),o?yn(t,i)&&Ot(n,"set",e,t):Ot(n,"add",e,t),this}function Ui(e){const t=he(this),{has:n,get:r}=Oa(t);let a=n.call(t,e);a||(e=he(e),a=n.call(t,e)),r&&r.call(t,e);const o=t.delete(e);return a&&Ot(t,"delete",e,void 0),o}function qi(){const e=he(this),t=e.size!==0,n=e.clear();return t&&Ot(e,"clear",void 0,void 0),n}function Hr(e,t){return function(r,a){const o=this,i=o.__v_raw,s=he(i),l=t?Yo:e?Jo:dr;return!e&&Ze(s,"iterate",vn),i.forEach((c,u)=>r.call(a,l(c),l(u),o))}}function Fr(e,t,n){return function(...r){const a=this.__v_raw,o=he(a),i=Rn(o),s=e==="entries"||e===Symbol.iterator&&i,l=e==="keys"&&i,c=a[e](...r),u=n?Yo:t?Jo:dr;return!t&&Ze(o,"iterate",l?vo:vn),{next(){const{value:f,done:d}=c.next();return d?{value:f,done:d}:{value:s?[u(f[0]),u(f[1])]:u(f),done:d}},[Symbol.iterator](){return this}}}}function Ft(e){return function(...t){return e==="delete"?!1:this}}function vf(){const e={get(o){return $r(this,o)},get size(){return Dr(this)},has:zr,add:ji,set:Bi,delete:Ui,clear:qi,forEach:Hr(!1,!1)},t={get(o){return $r(this,o,!1,!0)},get size(){return Dr(this)},has:zr,add:ji,set:Bi,delete:Ui,clear:qi,forEach:Hr(!1,!0)},n={get(o){return $r(this,o,!0)},get size(){return Dr(this,!0)},has(o){return zr.call(this,o,!0)},add:Ft("add"),set:Ft("set"),delete:Ft("delete"),clear:Ft("clear"),forEach:Hr(!0,!1)},r={get(o){return $r(this,o,!0,!0)},get size(){return Dr(this,!0)},has(o){return zr.call(this,o,!0)},add:Ft("add"),set:Ft("set"),delete:Ft("delete"),clear:Ft("clear"),forEach:Hr(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Fr(o,!1,!1),n[o]=Fr(o,!0,!1),t[o]=Fr(o,!1,!0),r[o]=Fr(o,!0,!0)}),[e,n,t,r]}const[gf,bf,yf,_f]=vf();function Go(e,t){const n=t?e?_f:yf:e?bf:gf;return(r,a,o)=>a==="__v_isReactive"?!e:a==="__v_isReadonly"?e:a==="__v_raw"?r:Reflect.get(pe(n,a)&&a in r?n:r,a,o)}const wf={get:Go(!1,!1)},kf={get:Go(!1,!0)},xf={get:Go(!0,!1)},Dl=new WeakMap,Hl=new WeakMap,Fl=new WeakMap,Ef=new WeakMap;function Cf(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Sf(e){return e.__v_skip||!Object.isExtensible(e)?0:Cf(Wu(e))}function Sr(e){return Dn(e)?e:Xo(e,!1,mf,wf,Dl)}function jl(e){return Xo(e,!1,hf,kf,Hl)}function Vn(e){return Xo(e,!0,pf,xf,Fl)}function Xo(e,t,n,r,a){if(!Se(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const o=a.get(e);if(o)return o;const i=Sf(e);if(i===0)return e;const s=new Proxy(e,i===2?r:n);return a.set(e,s),s}function Nn(e){return Dn(e)?Nn(e.__v_raw):!!(e&&e.__v_isReactive)}function Dn(e){return!!(e&&e.__v_isReadonly)}function fa(e){return!!(e&&e.__v_isShallow)}function Bl(e){return Nn(e)||Dn(e)}function he(e){const t=e&&e.__v_raw;return t?he(t):e}function Ul(e){return ca(e,"__v_skip",!0),e}const dr=e=>Se(e)?Sr(e):e,Jo=e=>Se(e)?Vn(e):e;function ql(e){Gt&&dt&&(e=he(e),Nl(e.dep||(e.dep=Ko())))}function Wl(e,t){e=he(e);const n=e.dep;n&&go(n)}function Fe(e){return!!(e&&e.__v_isRef===!0)}function ve(e){return Kl(e,!1)}function Qo(e){return Kl(e,!0)}function Kl(e,t){return Fe(e)?e:new Af(e,t)}class Af{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:he(t),this._value=n?t:dr(t)}get value(){return ql(this),this._value}set value(t){const n=this.__v_isShallow||fa(t)||Dn(t);t=n?t:he(t),yn(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:dr(t),Wl(this))}}function Q(e){return Fe(e)?e.value:e}const Tf={get:(e,t,n)=>Q(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const a=e[t];return Fe(a)&&!Fe(n)?(a.value=n,!0):Reflect.set(e,t,n,r)}};function Vl(e){return Nn(e)?e:new Proxy(e,Tf)}function Ia(e){const t=te(e)?new Array(e.length):{};for(const n in e)t[n]=Pf(e,n);return t}class Lf{constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return lf(he(this._object),this._key)}}function Pf(e,t,n){const r=e[t];return Fe(r)?r:new Lf(e,t,n)}class Of{constructor(t,n,r,a){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this._dirty=!0,this.effect=new Vo(t,()=>{this._dirty||(this._dirty=!0,Wl(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!a,this.__v_isReadonly=r}get value(){const t=he(this);return ql(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}function If(e,t,n=!1){let r,a;const o=ie(e);return o?(r=e,a=ht):(r=e.get,a=e.set),new Of(r,a,o||!a,n)}function Xt(e,t,n,r){let a;try{a=r?e(...r):e()}catch(o){Ar(o,t,n)}return a}function st(e,t,n,r){if(ie(e)){const o=Xt(e,t,n,r);return o&&Cl(o)&&o.catch(i=>{Ar(i,t,n)}),o}const a=[];for(let o=0;o>>1,a=qe[r],o=pr(a);owt&&qe.splice(t,1)}function $f(e){te(e)?Mn.push(...e):(!Lt||!Lt.includes(e,e.allowRecurse?un+1:un))&&Mn.push(e),Gl()}function Wi(e,t=mr?wt+1:0){for(;tpr(n)-pr(r)),un=0;une.id==null?1/0:e.id,zf=(e,t)=>{const n=pr(e)-pr(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Xl(e){bo=!1,mr=!0,qe.sort(zf);const t=ht;try{for(wt=0;wtge(h)?h.trim():h)),f&&(a=n.map(Yu))}let s,l=r[s=Ka(t)]||r[s=Ka(Et(t))];!l&&o&&(l=r[s=Ka(kn(t))]),l&&st(l,e,6,a);const c=r[s+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[s])return;e.emitted[s]=!0,st(c,e,6,a)}}function Jl(e,t,n=!1){const r=t.emitsCache,a=r.get(e);if(a!==void 0)return a;const o=e.emits;let i={},s=!1;if(!ie(e)){const l=c=>{const u=Jl(c,t,!0);u&&(s=!0,Ne(i,u))};!n&&t.mixins.length&&t.mixins.forEach(l),e.extends&&l(e.extends),e.mixins&&e.mixins.forEach(l)}return!o&&!s?(Se(e)&&r.set(e,null),null):(te(o)?o.forEach(l=>i[l]=null):Ne(i,o),Se(e)&&r.set(e,i),i)}function Na(e,t){return!e||!Er(t)?!1:(t=t.slice(2).replace(/Once$/,""),pe(e,t[0].toLowerCase()+t.slice(1))||pe(e,kn(t))||pe(e,t))}let Be=null,Ql=null;function ma(e){const t=Be;return Be=e,Ql=e&&e.type.__scopeId||null,t}function De(e,t=Be,n){if(!t||e._n)return e;const r=(...a)=>{r._d&&rs(-1);const o=ma(t);let i;try{i=e(...a)}finally{ma(o),r._d&&rs(1)}return i};return r._n=!0,r._c=!0,r._d=!0,r}function Ya(e){const{type:t,vnode:n,proxy:r,withProxy:a,props:o,propsOptions:[i],slots:s,attrs:l,emit:c,render:u,renderCache:f,data:d,setupState:h,ctx:w,inheritAttrs:k}=e;let E,_;const x=ma(e);try{if(n.shapeFlag&4){const b=a||r;E=ft(u.call(b,b,f,o,h,d,w)),_=l}else{const b=t;E=ft(b.length>1?b(o,{attrs:l,slots:s,emit:c}):b(o,null)),_=t.props?l:Hf(l)}}catch(b){sr.length=0,Ar(b,e,1),E=re(nt)}let v=E;if(_&&k!==!1){const b=Object.keys(_),{shapeFlag:P}=v;b.length&&P&7&&(i&&b.some(Uo)&&(_=Ff(_,i)),v=en(v,_))}return n.dirs&&(v=en(v),v.dirs=v.dirs?v.dirs.concat(n.dirs):n.dirs),n.transition&&(v.transition=n.transition),E=v,ma(x),E}const Hf=e=>{let t;for(const n in e)(n==="class"||n==="style"||Er(n))&&((t||(t={}))[n]=e[n]);return t},Ff=(e,t)=>{const n={};for(const r in e)(!Uo(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function jf(e,t,n){const{props:r,children:a,component:o}=e,{props:i,children:s,patchFlag:l}=t,c=o.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&l>=0){if(l&1024)return!0;if(l&16)return r?Ki(r,i,c):!!i;if(l&8){const u=t.dynamicProps;for(let f=0;fe.__isSuspense;function Zl(e,t){t&&t.pendingBranch?te(e)?t.effects.push(...e):t.effects.push(e):$f(e)}function ec(e,t){return ei(e,null,t)}const jr={};function lt(e,t,n){return ei(e,t,n)}function ei(e,t,{immediate:n,deep:r,flush:a,onTrack:o,onTrigger:i}=xe){var s;const l=Pl()===((s=Re)==null?void 0:s.scope)?Re:null;let c,u=!1,f=!1;if(Fe(e)?(c=()=>e.value,u=fa(e)):Nn(e)?(c=()=>e,r=!0):te(e)?(f=!0,u=e.some(b=>Nn(b)||fa(b)),c=()=>e.map(b=>{if(Fe(b))return b.value;if(Nn(b))return mn(b);if(ie(b))return Xt(b,l,2)})):ie(e)?t?c=()=>Xt(e,l,2):c=()=>{if(!(l&&l.isUnmounted))return d&&d(),st(e,l,3,[h])}:c=ht,t&&r){const b=c;c=()=>mn(b())}let d,h=b=>{d=x.onStop=()=>{Xt(b,l,4)}},w;if(jn)if(h=ht,t?n&&st(t,l,3,[c(),f?[]:void 0,h]):c(),a==="sync"){const b=zd();w=b.__watcherHandles||(b.__watcherHandles=[])}else return ht;let k=f?new Array(e.length).fill(jr):jr;const E=()=>{if(x.active)if(t){const b=x.run();(r||u||(f?b.some((P,B)=>yn(P,k[B])):yn(b,k)))&&(d&&d(),st(t,l,3,[b,k===jr?void 0:f&&k[0]===jr?[]:k,h]),k=b)}else x.run()};E.allowRecurse=!!t;let _;a==="sync"?_=E:a==="post"?_=()=>Je(E,l&&l.suspense):(E.pre=!0,l&&(E.id=l.uid),_=()=>Ra(E));const x=new Vo(c,_);t?n?E():k=x.run():a==="post"?Je(x.run.bind(x),l&&l.suspense):x.run();const v=()=>{x.stop(),l&&l.scope&&qo(l.scope.effects,x)};return w&&w.push(v),v}function qf(e,t,n){const r=this.proxy,a=ge(e)?e.includes(".")?tc(r,e):()=>r[e]:e.bind(r,r);let o;ie(t)?o=t:(o=t.handler,n=t);const i=Re;Fn(this);const s=ei(a,o.bind(r),n);return i?Fn(i):bn(),s}function tc(e,t){const n=t.split(".");return()=>{let r=e;for(let a=0;a{mn(n,t)});else if(Al(e))for(const n in e)mn(e[n],t);return e}function pa(e,t){const n=Be;if(n===null)return e;const r=Da(n)||n.proxy,a=e.dirs||(e.dirs=[]);for(let o=0;o{e.isMounted=!0}),Pr(()=>{e.isUnmounting=!0}),e}const at=[Function,Array],nc={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:at,onEnter:at,onAfterEnter:at,onEnterCancelled:at,onBeforeLeave:at,onLeave:at,onAfterLeave:at,onLeaveCancelled:at,onBeforeAppear:at,onAppear:at,onAfterAppear:at,onAppearCancelled:at},Kf={name:"BaseTransition",props:nc,setup(e,{slots:t}){const n=ai(),r=Wf();let a;return()=>{const o=t.default&&ac(t.default(),!0);if(!o||!o.length)return;let i=o[0];if(o.length>1){for(const k of o)if(k.type!==nt){i=k;break}}const s=he(e),{mode:l}=s;if(r.isLeaving)return Ga(i);const c=Vi(i);if(!c)return Ga(i);const u=yo(c,s,r,n);_o(c,u);const f=n.subTree,d=f&&Vi(f);let h=!1;const{getTransitionKey:w}=c.type;if(w){const k=w();a===void 0?a=k:k!==a&&(a=k,h=!0)}if(d&&d.type!==nt&&(!fn(c,d)||h)){const k=yo(d,s,r,n);if(_o(d,k),l==="out-in")return r.isLeaving=!0,k.afterLeave=()=>{r.isLeaving=!1,n.update.active!==!1&&n.update()},Ga(i);l==="in-out"&&c.type!==nt&&(k.delayLeave=(E,_,x)=>{const v=rc(r,d);v[String(d.key)]=d,E[Wt]=()=>{_(),E[Wt]=void 0,delete u.delayedLeave},u.delayedLeave=x})}return i}}},Vf=Kf;function rc(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function yo(e,t,n,r){const{appear:a,mode:o,persisted:i=!1,onBeforeEnter:s,onEnter:l,onAfterEnter:c,onEnterCancelled:u,onBeforeLeave:f,onLeave:d,onAfterLeave:h,onLeaveCancelled:w,onBeforeAppear:k,onAppear:E,onAfterAppear:_,onAppearCancelled:x}=t,v=String(e.key),b=rc(n,e),P=(g,M)=>{g&&st(g,r,9,M)},B=(g,M)=>{const R=M[1];P(g,M),te(g)?g.every(J=>J.length<=1)&&R():g.length<=1&&R()},I={mode:o,persisted:i,beforeEnter(g){let M=s;if(!n.isMounted)if(a)M=k||s;else return;g[Wt]&&g[Wt](!0);const R=b[v];R&&fn(e,R)&&R.el[Wt]&&R.el[Wt](),P(M,[g])},enter(g){let M=l,R=c,J=u;if(!n.isMounted)if(a)M=E||l,R=_||c,J=x||u;else return;let S=!1;const $=g[Br]=ae=>{S||(S=!0,ae?P(J,[g]):P(R,[g]),I.delayedLeave&&I.delayedLeave(),g[Br]=void 0)};M?B(M,[g,$]):$()},leave(g,M){const R=String(e.key);if(g[Br]&&g[Br](!0),n.isUnmounting)return M();P(f,[g]);let J=!1;const S=g[Wt]=$=>{J||(J=!0,M(),$?P(w,[g]):P(h,[g]),g[Wt]=void 0,b[R]===e&&delete b[R])};b[R]=e,d?B(d,[g,S]):S()},clone(g){return yo(g,t,n,r)}};return I}function Ga(e){if(Lr(e))return e=en(e),e.children=null,e}function Vi(e){return Lr(e)?e.children?e.children[0]:void 0:e}function _o(e,t){e.shapeFlag&6&&e.component?_o(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function ac(e,t=!1,n){let r=[],a=0;for(let o=0;o1)for(let o=0;oNe({name:e.name},t,{setup:e}))():e}const $n=e=>!!e.type.__asyncLoader;/*! #__NO_SIDE_EFFECTS__ */function it(e){ie(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:a=200,timeout:o,suspensible:i=!0,onError:s}=e;let l=null,c,u=0;const f=()=>(u++,l=null,d()),d=()=>{let h;return l||(h=l=t().catch(w=>{if(w=w instanceof Error?w:new Error(String(w)),s)return new Promise((k,E)=>{s(w,()=>k(f()),()=>E(w),u+1)});throw w}).then(w=>h!==l&&l?l:(w&&(w.__esModule||w[Symbol.toStringTag]==="Module")&&(w=w.default),c=w,w)))};return fe({name:"AsyncComponentWrapper",__asyncLoader:d,get __asyncResolved(){return c},setup(){const h=Re;if(c)return()=>Xa(c,h);const w=x=>{l=null,Ar(x,h,13,!r)};if(i&&h.suspense||jn)return d().then(x=>()=>Xa(x,h)).catch(x=>(w(x),()=>r?re(r,{error:x}):null));const k=ve(!1),E=ve(),_=ve(!!a);return a&&setTimeout(()=>{_.value=!1},a),o!=null&&setTimeout(()=>{if(!k.value&&!E.value){const x=new Error(`Async component timed out after ${o}ms.`);w(x),E.value=x}},o),d().then(()=>{k.value=!0,h.parent&&Lr(h.parent.vnode)&&Ra(h.parent.update)}).catch(x=>{w(x),E.value=x}),()=>{if(k.value&&c)return Xa(c,h);if(E.value&&r)return re(r,{error:E.value});if(n&&!_.value)return re(n)}}})}function Xa(e,t){const{ref:n,props:r,children:a,ce:o}=t.vnode,i=re(e,r,a);return i.ref=n,i.ce=o,delete t.vnode.ce,i}const Lr=e=>e.type.__isKeepAlive;function Yf(e,t){oc(e,"a",t)}function Gf(e,t){oc(e,"da",t)}function oc(e,t,n=Re){const r=e.__wdc||(e.__wdc=()=>{let a=n;for(;a;){if(a.isDeactivated)return;a=a.parent}return e()});if(Ma(t,r,n),n){let a=n.parent;for(;a&&a.parent;)Lr(a.parent.vnode)&&Xf(r,t,n,a),a=a.parent}}function Xf(e,t,n,r){const a=Ma(t,e,r,!0);$a(()=>{qo(r[t],a)},n)}function Ma(e,t,n=Re,r=!1){if(n){const a=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...i)=>{if(n.isUnmounted)return;Wn(),Fn(n);const s=st(t,n,e,i);return bn(),Kn(),s});return r?a.unshift(o):a.push(o),o}}const $t=e=>(t,n=Re)=>(!jn||e==="sp")&&Ma(e,(...r)=>t(...r),n),ic=$t("bm"),Ye=$t("m"),Jf=$t("bu"),Qf=$t("u"),Pr=$t("bum"),$a=$t("um"),Zf=$t("sp"),ed=$t("rtg"),td=$t("rtc");function nd(e,t=Re){Ma("ec",e,t)}const sc="components";function xt(e,t){return ad(sc,e,!0,t)||e}const rd=Symbol.for("v-ndc");function ad(e,t,n=!0,r=!1){const a=Be||Re;if(a){const o=a.type;if(e===sc){const s=Nd(o,!1);if(s&&(s===t||s===Et(t)||s===Pa(Et(t))))return o}const i=Yi(a[e]||o[e],t)||Yi(a.appContext[e],t);return!i&&r?o:i}}function Yi(e,t){return e&&(e[t]||e[Et(t)]||e[Pa(Et(t))])}function Zt(e,t,n,r){let a;const o=n&&n[r];if(te(e)||ge(e)){a=new Array(e.length);for(let i=0,s=e.length;it(i,s,void 0,o&&o[s]));else{const i=Object.keys(e);a=new Array(i.length);for(let s=0,l=i.length;sba(t)?!(t.type===nt||t.type===_e&&!lc(t.children)):!0)?e:null}const wo=e=>e?_c(e)?Da(e)||e.proxy:wo(e.parent):null,or=Ne(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>wo(e.parent),$root:e=>wo(e.root),$emit:e=>e.emit,$options:e=>ti(e),$forceUpdate:e=>e.f||(e.f=()=>Ra(e.update)),$nextTick:e=>e.n||(e.n=Tr.bind(e.proxy)),$watch:e=>qf.bind(e)}),Ja=(e,t)=>e!==xe&&!e.__isScriptSetup&&pe(e,t),od={get({_:e},t){const{ctx:n,setupState:r,data:a,props:o,accessCache:i,type:s,appContext:l}=e;let c;if(t[0]!=="$"){const h=i[t];if(h!==void 0)switch(h){case 1:return r[t];case 2:return a[t];case 4:return n[t];case 3:return o[t]}else{if(Ja(r,t))return i[t]=1,r[t];if(a!==xe&&pe(a,t))return i[t]=2,a[t];if((c=e.propsOptions[0])&&pe(c,t))return i[t]=3,o[t];if(n!==xe&&pe(n,t))return i[t]=4,n[t];ko&&(i[t]=0)}}const u=or[t];let f,d;if(u)return t==="$attrs"&&Ze(e,"get",t),u(e);if((f=s.__cssModules)&&(f=f[t]))return f;if(n!==xe&&pe(n,t))return i[t]=4,n[t];if(d=l.config.globalProperties,pe(d,t))return d[t]},set({_:e},t,n){const{data:r,setupState:a,ctx:o}=e;return Ja(a,t)?(a[t]=n,!0):r!==xe&&pe(r,t)?(r[t]=n,!0):pe(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(o[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:a,propsOptions:o}},i){let s;return!!n[i]||e!==xe&&pe(e,i)||Ja(t,i)||(s=o[0])&&pe(s,i)||pe(r,i)||pe(or,i)||pe(a.config.globalProperties,i)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:pe(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Gi(e){return te(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let ko=!0;function id(e){const t=ti(e),n=e.proxy,r=e.ctx;ko=!1,t.beforeCreate&&Xi(t.beforeCreate,e,"bc");const{data:a,computed:o,methods:i,watch:s,provide:l,inject:c,created:u,beforeMount:f,mounted:d,beforeUpdate:h,updated:w,activated:k,deactivated:E,beforeDestroy:_,beforeUnmount:x,destroyed:v,unmounted:b,render:P,renderTracked:B,renderTriggered:I,errorCaptured:g,serverPrefetch:M,expose:R,inheritAttrs:J,components:S,directives:$,filters:ae}=t;if(c&&sd(c,r,null),i)for(const G in i){const W=i[G];ie(W)&&(r[G]=W.bind(n))}if(a){const G=a.call(n,n);Se(G)&&(e.data=Sr(G))}if(ko=!0,o)for(const G in o){const W=o[G],$e=ie(W)?W.bind(n,n):ie(W.get)?W.get.bind(n,n):ht,je=!ie(W)&&ie(W.set)?W.set.bind(n):ht,Ge=D({get:$e,set:je});Object.defineProperty(r,G,{enumerable:!0,configurable:!0,get:()=>Ge.value,set:Ue=>Ge.value=Ue})}if(s)for(const G in s)cc(s[G],r,n,G);if(l){const G=ie(l)?l.call(n):l;Reflect.ownKeys(G).forEach(W=>{gn(W,G[W])})}u&&Xi(u,e,"c");function N(G,W){te(W)?W.forEach($e=>G($e.bind(n))):W&&G(W.bind(n))}if(N(ic,f),N(Ye,d),N(Jf,h),N(Qf,w),N(Yf,k),N(Gf,E),N(nd,g),N(td,B),N(ed,I),N(Pr,x),N($a,b),N(Zf,M),te(R))if(R.length){const G=e.exposed||(e.exposed={});R.forEach(W=>{Object.defineProperty(G,W,{get:()=>n[W],set:$e=>n[W]=$e})})}else e.exposed||(e.exposed={});P&&e.render===ht&&(e.render=P),J!=null&&(e.inheritAttrs=J),S&&(e.components=S),$&&(e.directives=$)}function sd(e,t,n=ht){te(e)&&(e=xo(e));for(const r in e){const a=e[r];let o;Se(a)?"default"in a?o=Ie(a.from||r,a.default,!0):o=Ie(a.from||r):o=Ie(a),Fe(o)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>o.value,set:i=>o.value=i}):t[r]=o}}function Xi(e,t,n){st(te(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function cc(e,t,n,r){const a=r.includes(".")?tc(n,r):()=>n[r];if(ge(e)){const o=t[e];ie(o)&<(a,o)}else if(ie(e))lt(a,e.bind(n));else if(Se(e))if(te(e))e.forEach(o=>cc(o,t,n,r));else{const o=ie(e.handler)?e.handler.bind(n):t[e.handler];ie(o)&<(a,o,e)}}function ti(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:a,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,s=o.get(t);let l;return s?l=s:!a.length&&!n&&!r?l=t:(l={},a.length&&a.forEach(c=>ha(l,c,i,!0)),ha(l,t,i)),Se(t)&&o.set(t,l),l}function ha(e,t,n,r=!1){const{mixins:a,extends:o}=t;o&&ha(e,o,n,!0),a&&a.forEach(i=>ha(e,i,n,!0));for(const i in t)if(!(r&&i==="expose")){const s=ld[i]||n&&n[i];e[i]=s?s(e[i],t[i]):t[i]}return e}const ld={data:Ji,props:Qi,emits:Qi,methods:nr,computed:nr,beforeCreate:We,created:We,beforeMount:We,mounted:We,beforeUpdate:We,updated:We,beforeDestroy:We,beforeUnmount:We,destroyed:We,unmounted:We,activated:We,deactivated:We,errorCaptured:We,serverPrefetch:We,components:nr,directives:nr,watch:ud,provide:Ji,inject:cd};function Ji(e,t){return t?e?function(){return Ne(ie(e)?e.call(this,this):e,ie(t)?t.call(this,this):t)}:t:e}function cd(e,t){return nr(xo(e),xo(t))}function xo(e){if(te(e)){const t={};for(let n=0;n1)return n&&ie(t)?t.call(r&&r.proxy):t}}function md(e,t,n,r=!1){const a={},o={};ca(o,za,1),e.propsDefaults=Object.create(null),fc(e,t,a,o);for(const i in e.propsOptions[0])i in a||(a[i]=void 0);n?e.props=r?a:jl(a):e.type.props?e.props=a:e.props=o,e.attrs=o}function pd(e,t,n,r){const{props:a,attrs:o,vnode:{patchFlag:i}}=e,s=he(a),[l]=e.propsOptions;let c=!1;if((r||i>0)&&!(i&16)){if(i&8){const u=e.vnode.dynamicProps;for(let f=0;f{l=!0;const[d,h]=dc(f,t,!0);Ne(i,d),h&&s.push(...h)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!o&&!l)return Se(e)&&r.set(e,In),In;if(te(o))for(let u=0;u-1,h[1]=k<0||w-1||pe(h,"default"))&&s.push(f)}}}const c=[i,s];return Se(e)&&r.set(e,c),c}function Zi(e){return e[0]!=="$"}function es(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:e===null?"null":""}function ts(e,t){return es(e)===es(t)}function ns(e,t){return te(t)?t.findIndex(n=>ts(n,e)):ie(t)&&ts(t,e)?0:-1}const mc=e=>e[0]==="_"||e==="$stable",ni=e=>te(e)?e.map(ft):[ft(e)],hd=(e,t,n)=>{if(t._n)return t;const r=De((...a)=>ni(t(...a)),n);return r._c=!1,r},pc=(e,t,n)=>{const r=e._ctx;for(const a in e){if(mc(a))continue;const o=e[a];if(ie(o))t[a]=hd(a,o,r);else if(o!=null){const i=ni(o);t[a]=()=>i}}},hc=(e,t)=>{const n=ni(t);e.slots.default=()=>n},vd=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=he(t),ca(t,"_",n)):pc(t,e.slots={})}else e.slots={},t&&hc(e,t);ca(e.slots,za,1)},gd=(e,t,n)=>{const{vnode:r,slots:a}=e;let o=!0,i=xe;if(r.shapeFlag&32){const s=t._;s?n&&s===1?o=!1:(Ne(a,t),!n&&s===1&&delete a._):(o=!t.$stable,pc(t,a)),i=t}else t&&(hc(e,t),i={default:1});if(o)for(const s in a)!mc(s)&&i[s]==null&&delete a[s]};function ga(e,t,n,r,a=!1){if(te(e)){e.forEach((d,h)=>ga(d,t&&(te(t)?t[h]:t),n,r,a));return}if($n(r)&&!a)return;const o=r.shapeFlag&4?Da(r.component)||r.component.proxy:r.el,i=a?null:o,{i:s,r:l}=e,c=t&&t.r,u=s.refs===xe?s.refs={}:s.refs,f=s.setupState;if(c!=null&&c!==l&&(ge(c)?(u[c]=null,pe(f,c)&&(f[c]=null)):Fe(c)&&(c.value=null)),ie(l))Xt(l,s,12,[i,u]);else{const d=ge(l),h=Fe(l);if(d||h){const w=()=>{if(e.f){const k=d?pe(f,l)?f[l]:u[l]:l.value;a?te(k)&&qo(k,o):te(k)?k.includes(o)||k.push(o):d?(u[l]=[o],pe(f,l)&&(f[l]=u[l])):(l.value=[o],e.k&&(u[e.k]=l.value))}else d?(u[l]=i,pe(f,l)&&(f[l]=i)):h&&(l.value=i,e.k&&(u[e.k]=i))};i?(w.id=-1,Je(w,n)):w()}}}let jt=!1;const Ur=e=>/svg/.test(e.namespaceURI)&&e.tagName!=="foreignObject",qr=e=>e.nodeType===8;function bd(e){const{mt:t,p:n,o:{patchProp:r,createText:a,nextSibling:o,parentNode:i,remove:s,insert:l,createComment:c}}=e,u=(v,b)=>{if(!b.hasChildNodes()){n(null,v,b),da(),b._vnode=v;return}jt=!1,f(b.firstChild,v,null,null,null),da(),b._vnode=v,jt&&console.error("Hydration completed but contains mismatches.")},f=(v,b,P,B,I,g=!1)=>{const M=qr(v)&&v.data==="[",R=()=>k(v,b,P,B,I,M),{type:J,ref:S,shapeFlag:$,patchFlag:ae}=b;let se=v.nodeType;b.el=v,ae===-2&&(g=!1,b.dynamicChildren=null);let N=null;switch(J){case Hn:se!==3?b.children===""?(l(b.el=a(""),i(v),v),N=v):N=R():(v.data!==b.children&&(jt=!0,v.data=b.children),N=o(v));break;case nt:if(se!==8||M)if(v.tagName.toLowerCase()==="template"){const G=b.el.content.firstChild;_(G,v,P),b.el=v=G,N=o(v)}else N=R();else N=o(v);break;case ir:if(M&&(v=o(v),se=v.nodeType),se===1||se===3){N=v;const G=!b.children.length;for(let W=0;W{g=g||!!b.dynamicChildren;const{type:M,props:R,patchFlag:J,shapeFlag:S,dirs:$,transition:ae}=b,se=M==="input"&&$||M==="option";if(se||J!==-1){if($&&_t(b,null,P,"created"),R)if(se||!g||J&48)for(const W in R)(se&&W.endsWith("value")||Er(W)&&!ar(W))&&r(v,W,null,R[W],!1,void 0,P);else R.onClick&&r(v,"onClick",null,R.onClick,!1,void 0,P);let N;(N=R&&R.onVnodeBeforeMount)&&ot(N,P,b);let G=!1;if(x(v)){G=vc(B,ae)&&P&&P.vnode.props&&P.vnode.props.appear;const W=v.content.firstChild;G&&ae.beforeEnter(W),_(W,v,P),b.el=v=W}if($&&_t(b,null,P,"beforeMount"),((N=R&&R.onVnodeMounted)||$||G)&&Zl(()=>{N&&ot(N,P,b),G&&ae.enter(v),$&&_t(b,null,P,"mounted")},B),S&16&&!(R&&(R.innerHTML||R.textContent))){let W=h(v.firstChild,b,v,P,B,I,g);for(;W;){jt=!0;const $e=W;W=W.nextSibling,s($e)}}else S&8&&v.textContent!==b.children&&(jt=!0,v.textContent=b.children)}return v.nextSibling},h=(v,b,P,B,I,g,M)=>{M=M||!!b.dynamicChildren;const R=b.children,J=R.length;for(let S=0;S{const{slotScopeIds:M}=b;M&&(I=I?I.concat(M):M);const R=i(v),J=h(o(v),b,R,P,B,I,g);return J&&qr(J)&&J.data==="]"?o(b.anchor=J):(jt=!0,l(b.anchor=c("]"),R,J),J)},k=(v,b,P,B,I,g)=>{if(jt=!0,b.el=null,g){const J=E(v);for(;;){const S=o(v);if(S&&S!==J)s(S);else break}}const M=o(v),R=i(v);return s(v),n(null,b,R,M,P,B,Ur(R),I),M},E=(v,b="[",P="]")=>{let B=0;for(;v;)if(v=o(v),v&&qr(v)&&(v.data===b&&B++,v.data===P)){if(B===0)return o(v);B--}return v},_=(v,b,P)=>{const B=b.parentNode;B&&B.replaceChild(v,b);let I=P;for(;I;)I.vnode.el===b&&(I.vnode.el=v,I.subTree.el=v),I=I.parent},x=v=>v.nodeType===1&&v.tagName.toLowerCase()==="template";return[u,f]}const Je=Zl;function yd(e){return _d(e,bd)}function _d(e,t){const n=po();n.__VUE__=!0;const{insert:r,remove:a,patchProp:o,createElement:i,createText:s,createComment:l,setText:c,setElementText:u,parentNode:f,nextSibling:d,setScopeId:h=ht,insertStaticContent:w}=e,k=(m,p,y,C=null,T=null,L=null,j=!1,z=null,F=!!p.dynamicChildren)=>{if(m===p)return;m&&!fn(m,p)&&(C=A(m),Ue(m,T,L,!0),m=null),p.patchFlag===-2&&(F=!1,p.dynamicChildren=null);const{type:O,ref:Z,shapeFlag:V}=p;switch(O){case Hn:E(m,p,y,C);break;case nt:_(m,p,y,C);break;case ir:m==null&&x(p,y,C,j);break;case _e:S(m,p,y,C,T,L,j,z,F);break;default:V&1?P(m,p,y,C,T,L,j,z,F):V&6?$(m,p,y,C,T,L,j,z,F):(V&64||V&128)&&O.process(m,p,y,C,T,L,j,z,F,H)}Z!=null&&T&&ga(Z,m&&m.ref,L,p||m,!p)},E=(m,p,y,C)=>{if(m==null)r(p.el=s(p.children),y,C);else{const T=p.el=m.el;p.children!==m.children&&c(T,p.children)}},_=(m,p,y,C)=>{m==null?r(p.el=l(p.children||""),y,C):p.el=m.el},x=(m,p,y,C)=>{[m.el,m.anchor]=w(m.children,p,y,C,m.el,m.anchor)},v=({el:m,anchor:p},y,C)=>{let T;for(;m&&m!==p;)T=d(m),r(m,y,C),m=T;r(p,y,C)},b=({el:m,anchor:p})=>{let y;for(;m&&m!==p;)y=d(m),a(m),m=y;a(p)},P=(m,p,y,C,T,L,j,z,F)=>{j=j||p.type==="svg",m==null?B(p,y,C,T,L,j,z,F):M(m,p,T,L,j,z,F)},B=(m,p,y,C,T,L,j,z)=>{let F,O;const{type:Z,props:V,shapeFlag:ee,transition:oe,dirs:ce}=m;if(F=m.el=i(m.type,L,V&&V.is,V),ee&8?u(F,m.children):ee&16&&g(m.children,F,null,C,T,L&&Z!=="foreignObject",j,z),ce&&_t(m,null,C,"created"),I(F,m,m.scopeId,j,C),V){for(const ye in V)ye!=="value"&&!ar(ye)&&o(F,ye,null,V[ye],L,m.children,C,T,ze);"value"in V&&o(F,"value",null,V.value),(O=V.onVnodeBeforeMount)&&ot(O,C,m)}ce&&_t(m,null,C,"beforeMount");const we=vc(T,oe);we&&oe.beforeEnter(F),r(F,p,y),((O=V&&V.onVnodeMounted)||we||ce)&&Je(()=>{O&&ot(O,C,m),we&&oe.enter(F),ce&&_t(m,null,C,"mounted")},T)},I=(m,p,y,C,T)=>{if(y&&h(m,y),C)for(let L=0;L{for(let O=F;O{const z=p.el=m.el;let{patchFlag:F,dynamicChildren:O,dirs:Z}=p;F|=m.patchFlag&16;const V=m.props||xe,ee=p.props||xe;let oe;y&&on(y,!1),(oe=ee.onVnodeBeforeUpdate)&&ot(oe,y,p,m),Z&&_t(p,m,y,"beforeUpdate"),y&&on(y,!0);const ce=T&&p.type!=="foreignObject";if(O?R(m.dynamicChildren,O,z,y,C,ce,L):j||W(m,p,z,null,y,C,ce,L,!1),F>0){if(F&16)J(z,p,V,ee,y,C,T);else if(F&2&&V.class!==ee.class&&o(z,"class",null,ee.class,T),F&4&&o(z,"style",V.style,ee.style,T),F&8){const we=p.dynamicProps;for(let ye=0;ye{oe&&ot(oe,y,p,m),Z&&_t(p,m,y,"updated")},C)},R=(m,p,y,C,T,L,j)=>{for(let z=0;z{if(y!==C){if(y!==xe)for(const z in y)!ar(z)&&!(z in C)&&o(m,z,y[z],null,j,p.children,T,L,ze);for(const z in C){if(ar(z))continue;const F=C[z],O=y[z];F!==O&&z!=="value"&&o(m,z,O,F,j,p.children,T,L,ze)}"value"in C&&o(m,"value",y.value,C.value)}},S=(m,p,y,C,T,L,j,z,F)=>{const O=p.el=m?m.el:s(""),Z=p.anchor=m?m.anchor:s("");let{patchFlag:V,dynamicChildren:ee,slotScopeIds:oe}=p;oe&&(z=z?z.concat(oe):oe),m==null?(r(O,y,C),r(Z,y,C),g(p.children,y,Z,T,L,j,z,F)):V>0&&V&64&&ee&&m.dynamicChildren?(R(m.dynamicChildren,ee,y,T,L,j,z),(p.key!=null||T&&p===T.subTree)&&gc(m,p,!0)):W(m,p,y,Z,T,L,j,z,F)},$=(m,p,y,C,T,L,j,z,F)=>{p.slotScopeIds=z,m==null?p.shapeFlag&512?T.ctx.activate(p,y,C,j,F):ae(p,y,C,T,L,j,F):se(m,p,F)},ae=(m,p,y,C,T,L,j)=>{const z=m.component=Ld(m,C,T);if(Lr(m)&&(z.ctx.renderer=H),Pd(z),z.asyncDep){if(T&&T.registerDep(z,N),!m.el){const F=z.subTree=re(nt);_(null,F,p,y)}return}N(z,m,p,y,T,L,j)},se=(m,p,y)=>{const C=p.component=m.component;if(jf(m,p,y))if(C.asyncDep&&!C.asyncResolved){G(C,p,y);return}else C.next=p,Mf(C.update),C.update();else p.el=m.el,C.vnode=p},N=(m,p,y,C,T,L,j)=>{const z=()=>{if(m.isMounted){let{next:Z,bu:V,u:ee,parent:oe,vnode:ce}=m,we=Z,ye;on(m,!1),Z?(Z.el=ce.el,G(m,Z,j)):Z=ce,V&&Va(V),(ye=Z.props&&Z.props.onVnodeBeforeUpdate)&&ot(ye,oe,Z,ce),on(m,!0);const Pe=Ya(m),ut=m.subTree;m.subTree=Pe,k(ut,Pe,f(ut.el),A(ut),m,T,L),Z.el=Pe.el,we===null&&Bf(m,Pe.el),ee&&Je(ee,T),(ye=Z.props&&Z.props.onVnodeUpdated)&&Je(()=>ot(ye,oe,Z,ce),T)}else{let Z;const{el:V,props:ee}=p,{bm:oe,m:ce,parent:we}=m,ye=$n(p);if(on(m,!1),oe&&Va(oe),!ye&&(Z=ee&&ee.onVnodeBeforeMount)&&ot(Z,we,p),on(m,!0),V&&de){const Pe=()=>{m.subTree=Ya(m),de(V,m.subTree,m,T,null)};ye?p.type.__asyncLoader().then(()=>!m.isUnmounted&&Pe()):Pe()}else{const Pe=m.subTree=Ya(m);k(null,Pe,y,C,m,T,L),p.el=Pe.el}if(ce&&Je(ce,T),!ye&&(Z=ee&&ee.onVnodeMounted)){const Pe=p;Je(()=>ot(Z,we,Pe),T)}(p.shapeFlag&256||we&&$n(we.vnode)&&we.vnode.shapeFlag&256)&&m.a&&Je(m.a,T),m.isMounted=!0,p=y=C=null}},F=m.effect=new Vo(z,()=>Ra(O),m.scope),O=m.update=()=>F.run();O.id=m.uid,on(m,!0),O()},G=(m,p,y)=>{p.component=m;const C=m.vnode.props;m.vnode=p,m.next=null,pd(m,p.props,C,y),gd(m,p.children,y),Wn(),Wi(),Kn()},W=(m,p,y,C,T,L,j,z,F=!1)=>{const O=m&&m.children,Z=m?m.shapeFlag:0,V=p.children,{patchFlag:ee,shapeFlag:oe}=p;if(ee>0){if(ee&128){je(O,V,y,C,T,L,j,z,F);return}else if(ee&256){$e(O,V,y,C,T,L,j,z,F);return}}oe&8?(Z&16&&ze(O,T,L),V!==O&&u(y,V)):Z&16?oe&16?je(O,V,y,C,T,L,j,z,F):ze(O,T,L,!0):(Z&8&&u(y,""),oe&16&&g(V,y,C,T,L,j,z,F))},$e=(m,p,y,C,T,L,j,z,F)=>{m=m||In,p=p||In;const O=m.length,Z=p.length,V=Math.min(O,Z);let ee;for(ee=0;eeZ?ze(m,T,L,!0,!1,V):g(p,y,C,T,L,j,z,F,V)},je=(m,p,y,C,T,L,j,z,F)=>{let O=0;const Z=p.length;let V=m.length-1,ee=Z-1;for(;O<=V&&O<=ee;){const oe=m[O],ce=p[O]=F?Kt(p[O]):ft(p[O]);if(fn(oe,ce))k(oe,ce,y,null,T,L,j,z,F);else break;O++}for(;O<=V&&O<=ee;){const oe=m[V],ce=p[ee]=F?Kt(p[ee]):ft(p[ee]);if(fn(oe,ce))k(oe,ce,y,null,T,L,j,z,F);else break;V--,ee--}if(O>V){if(O<=ee){const oe=ee+1,ce=oeee)for(;O<=V;)Ue(m[O],T,L,!0),O++;else{const oe=O,ce=O,we=new Map;for(O=ce;O<=ee;O++){const et=p[O]=F?Kt(p[O]):ft(p[O]);et.key!=null&&we.set(et.key,O)}let ye,Pe=0;const ut=ee-ce+1;let Cn=!1,Ri=0;const Xn=new Array(ut);for(O=0;O=ut){Ue(et,T,L,!0);continue}let yt;if(et.key!=null)yt=we.get(et.key);else for(ye=ce;ye<=ee;ye++)if(Xn[ye-ce]===0&&fn(et,p[ye])){yt=ye;break}yt===void 0?Ue(et,T,L,!0):(Xn[yt-ce]=O+1,yt>=Ri?Ri=yt:Cn=!0,k(et,p[yt],y,null,T,L,j,z,F),Pe++)}const Ni=Cn?wd(Xn):In;for(ye=Ni.length-1,O=ut-1;O>=0;O--){const et=ce+O,yt=p[et],Mi=et+1{const{el:L,type:j,transition:z,children:F,shapeFlag:O}=m;if(O&6){Ge(m.component.subTree,p,y,C);return}if(O&128){m.suspense.move(p,y,C);return}if(O&64){j.move(m,p,y,H);return}if(j===_e){r(L,p,y);for(let V=0;Vz.enter(L),T);else{const{leave:V,delayLeave:ee,afterLeave:oe}=z,ce=()=>r(L,p,y),we=()=>{V(L,()=>{ce(),oe&&oe()})};ee?ee(L,ce,we):we()}else r(L,p,y)},Ue=(m,p,y,C=!1,T=!1)=>{const{type:L,props:j,ref:z,children:F,dynamicChildren:O,shapeFlag:Z,patchFlag:V,dirs:ee}=m;if(z!=null&&ga(z,null,y,m,!0),Z&256){p.ctx.deactivate(m);return}const oe=Z&1&&ee,ce=!$n(m);let we;if(ce&&(we=j&&j.onVnodeBeforeUnmount)&&ot(we,p,m),Z&6)bt(m.component,y,C);else{if(Z&128){m.suspense.unmount(y,C);return}oe&&_t(m,null,p,"beforeUnmount"),Z&64?m.type.remove(m,p,y,T,H,C):O&&(L!==_e||V>0&&V&64)?ze(O,p,y,!1,!0):(L===_e&&V&384||!T&&Z&16)&&ze(F,p,y),C&&Dt(m)}(ce&&(we=j&&j.onVnodeUnmounted)||oe)&&Je(()=>{we&&ot(we,p,m),oe&&_t(m,null,p,"unmounted")},y)},Dt=m=>{const{type:p,el:y,anchor:C,transition:T}=m;if(p===_e){Ht(y,C);return}if(p===ir){b(m);return}const L=()=>{a(y),T&&!T.persisted&&T.afterLeave&&T.afterLeave()};if(m.shapeFlag&1&&T&&!T.persisted){const{leave:j,delayLeave:z}=T,F=()=>j(y,L);z?z(m.el,L,F):F()}else L()},Ht=(m,p)=>{let y;for(;m!==p;)y=d(m),a(m),m=y;a(p)},bt=(m,p,y)=>{const{bum:C,scope:T,update:L,subTree:j,um:z}=m;C&&Va(C),T.stop(),L&&(L.active=!1,Ue(j,m,p,y)),z&&Je(z,p),Je(()=>{m.isUnmounted=!0},p),p&&p.pendingBranch&&!p.isUnmounted&&m.asyncDep&&!m.asyncResolved&&m.suspenseId===p.pendingId&&(p.deps--,p.deps===0&&p.resolve())},ze=(m,p,y,C=!1,T=!1,L=0)=>{for(let j=L;jm.shapeFlag&6?A(m.component.subTree):m.shapeFlag&128?m.suspense.next():d(m.anchor||m.el),U=(m,p,y)=>{m==null?p._vnode&&Ue(p._vnode,null,null,!0):k(p._vnode||null,m,p,null,null,null,y),Wi(),da(),p._vnode=m},H={p:k,um:Ue,m:Ge,r:Dt,mt:ae,mc:g,pc:W,pbc:R,n:A,o:e};let X,de;return t&&([X,de]=t(H)),{render:U,hydrate:X,createApp:dd(U,X)}}function on({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function vc(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function gc(e,t,n=!1){const r=e.children,a=t.children;if(te(r)&&te(a))for(let o=0;o>1,e[n[s]]0&&(t[r]=n[o-1]),n[o]=r)}}for(o=n.length,i=n[o-1];o-- >0;)n[o]=i,i=t[i];return n}const kd=e=>e.__isTeleport,_e=Symbol.for("v-fgt"),Hn=Symbol.for("v-txt"),nt=Symbol.for("v-cmt"),ir=Symbol.for("v-stc"),sr=[];let mt=null;function q(e=!1){sr.push(mt=e?null:[])}function xd(){sr.pop(),mt=sr[sr.length-1]||null}let hr=1;function rs(e){hr+=e}function bc(e){return e.dynamicChildren=hr>0?mt||In:null,xd(),hr>0&&mt&&mt.push(e),e}function ne(e,t,n,r,a,o){return bc(le(e,t,n,r,a,o,!0))}function Oe(e,t,n,r,a){return bc(re(e,t,n,r,a,!0))}function ba(e){return e?e.__v_isVNode===!0:!1}function fn(e,t){return e.type===t.type&&e.key===t.key}const za="__vInternal",yc=({key:e})=>e??null,oa=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ge(e)||Fe(e)||ie(e)?{i:Be,r:e,k:t,f:!!n}:e:null);function le(e,t=null,n=null,r=0,a=null,o=e===_e?0:1,i=!1,s=!1){const l={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&yc(t),ref:t&&oa(t),scopeId:Ql,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:r,dynamicProps:a,dynamicChildren:null,appContext:null,ctx:Be};return s?(ri(l,n),o&128&&e.normalize(l)):n&&(l.shapeFlag|=ge(n)?8:16),hr>0&&!i&&mt&&(l.patchFlag>0||o&6)&&l.patchFlag!==32&&mt.push(l),l}const re=Ed;function Ed(e,t=null,n=null,r=0,a=null,o=!1){if((!e||e===rd)&&(e=nt),ba(e)){const s=en(e,t,!0);return n&&ri(s,n),hr>0&&!o&&mt&&(s.shapeFlag&6?mt[mt.indexOf(e)]=s:mt.push(s)),s.patchFlag|=-2,s}if(Md(e)&&(e=e.__vccOpts),t){t=Cd(t);let{class:s,style:l}=t;s&&!ge(s)&&(t.class=Qe(s)),Se(l)&&(Bl(l)&&!te(l)&&(l=Ne({},l)),t.style=Cr(l))}const i=ge(e)?1:Uf(e)?128:kd(e)?64:Se(e)?4:ie(e)?2:0;return le(e,t,n,r,a,i,o,!0)}function Cd(e){return e?Bl(e)||za in e?Ne({},e):e:null}function en(e,t,n=!1){const{props:r,ref:a,patchFlag:o,children:i}=e,s=t?Co(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:s,key:s&&yc(s),ref:t&&t.ref?n&&a?te(a)?a.concat(oa(t)):[a,oa(t)]:oa(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:i,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==_e?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&en(e.ssContent),ssFallback:e.ssFallback&&en(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Ct(e=" ",t=0){return re(Hn,null,e,t)}function Sd(e,t){const n=re(ir,null,e);return n.staticCount=t,n}function Le(e="",t=!1){return t?(q(),Oe(nt,null,e)):re(nt,null,e)}function ft(e){return e==null||typeof e=="boolean"?re(nt):te(e)?re(_e,null,e.slice()):typeof e=="object"?Kt(e):re(Hn,null,String(e))}function Kt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:en(e)}function ri(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(te(t))n=16;else if(typeof t=="object")if(r&65){const a=t.default;a&&(a._c&&(a._d=!1),ri(e,a()),a._c&&(a._d=!0));return}else{n=32;const a=t._;!a&&!(za in t)?t._ctx=Be:a===3&&Be&&(Be.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else ie(t)?(t={default:t,_ctx:Be},n=32):(t=String(t),r&64?(n=16,t=[Ct(t)]):n=8);e.children=t,e.shapeFlag|=n}function Co(...e){const t={};for(let n=0;nRe||Be;let oi,Sn,as="__VUE_INSTANCE_SETTERS__";(Sn=po()[as])||(Sn=po()[as]=[]),Sn.push(e=>Re=e),oi=e=>{Sn.length>1?Sn.forEach(t=>t(e)):Sn[0](e)};const Fn=e=>{oi(e),e.scope.on()},bn=()=>{Re&&Re.scope.off(),oi(null)};function _c(e){return e.vnode.shapeFlag&4}let jn=!1;function Pd(e,t=!1){jn=t;const{props:n,children:r}=e.vnode,a=_c(e);md(e,n,a,t),vd(e,r);const o=a?Od(e,t):void 0;return jn=!1,o}function Od(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Ul(new Proxy(e.ctx,od));const{setup:r}=n;if(r){const a=e.setupContext=r.length>1?Rd(e):null;Fn(e),Wn();const o=Xt(r,e,0,[e.props,a]);if(Kn(),bn(),Cl(o)){if(o.then(bn,bn),t)return o.then(i=>{os(e,i,t)}).catch(i=>{Ar(i,e,0)});e.asyncDep=o}else os(e,o,t)}else wc(e,t)}function os(e,t,n){ie(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Se(t)&&(e.setupState=Vl(t)),wc(e,n)}let is;function wc(e,t,n){const r=e.type;if(!e.render){if(!t&&is&&!r.render){const a=r.template||ti(e).template;if(a){const{isCustomElement:o,compilerOptions:i}=e.appContext.config,{delimiters:s,compilerOptions:l}=r,c=Ne(Ne({isCustomElement:o,delimiters:s},i),l);r.render=is(a,c)}}e.render=r.render||ht}{Fn(e),Wn();try{id(e)}finally{Kn(),bn()}}}function Id(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return Ze(e,"get","$attrs"),t[n]}}))}function Rd(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return Id(e)},slots:e.slots,emit:e.emit,expose:t}}function Da(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Vl(Ul(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in or)return or[n](e)},has(t,n){return n in t||n in or}}))}function Nd(e,t=!0){return ie(e)?e.displayName||e.name:e.name||t&&e.__name}function Md(e){return ie(e)&&"__vccOpts"in e}const D=(e,t)=>If(e,t,jn);function ue(e,t,n){const r=arguments.length;return r===2?Se(t)&&!te(t)?ba(t)?re(e,null,[t]):re(e,t):re(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&ba(n)&&(n=[n]),re(e,t,n))}const $d=Symbol.for("v-scx"),zd=()=>Ie($d),Dd="3.3.7",Hd="http://www.w3.org/2000/svg",dn=typeof document<"u"?document:null,ss=dn&&dn.createElement("template"),Fd={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const a=t?dn.createElementNS(Hd,e):dn.createElement(e,n?{is:n}:void 0);return e==="select"&&r&&r.multiple!=null&&a.setAttribute("multiple",r.multiple),a},createText:e=>dn.createTextNode(e),createComment:e=>dn.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>dn.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,a,o){const i=n?n.previousSibling:t.lastChild;if(a&&(a===o||a.nextSibling))for(;t.insertBefore(a.cloneNode(!0),n),!(a===o||!(a=a.nextSibling)););else{ss.innerHTML=r?`${e}`:e;const s=ss.content;if(r){const l=s.firstChild;for(;l.firstChild;)s.appendChild(l.firstChild);s.removeChild(l)}t.insertBefore(s,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Bt="transition",Jn="animation",vr=Symbol("_vtc"),Or=(e,{slots:t})=>ue(Vf,jd(e),t);Or.displayName="Transition";const kc={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Or.props=Ne({},nc,kc);const sn=(e,t=[])=>{te(e)?e.forEach(n=>n(...t)):e&&e(...t)},ls=e=>e?te(e)?e.some(t=>t.length>1):e.length>1:!1;function jd(e){const t={};for(const S in e)S in kc||(t[S]=e[S]);if(e.css===!1)return t;const{name:n="v",type:r,duration:a,enterFromClass:o=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:s=`${n}-enter-to`,appearFromClass:l=o,appearActiveClass:c=i,appearToClass:u=s,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:d=`${n}-leave-active`,leaveToClass:h=`${n}-leave-to`}=e,w=Bd(a),k=w&&w[0],E=w&&w[1],{onBeforeEnter:_,onEnter:x,onEnterCancelled:v,onLeave:b,onLeaveCancelled:P,onBeforeAppear:B=_,onAppear:I=x,onAppearCancelled:g=v}=t,M=(S,$,ae)=>{ln(S,$?u:s),ln(S,$?c:i),ae&&ae()},R=(S,$)=>{S._isLeaving=!1,ln(S,f),ln(S,h),ln(S,d),$&&$()},J=S=>($,ae)=>{const se=S?I:x,N=()=>M($,S,ae);sn(se,[$,N]),cs(()=>{ln($,S?l:o),Ut($,S?u:s),ls(se)||us($,r,k,N)})};return Ne(t,{onBeforeEnter(S){sn(_,[S]),Ut(S,o),Ut(S,i)},onBeforeAppear(S){sn(B,[S]),Ut(S,l),Ut(S,c)},onEnter:J(!1),onAppear:J(!0),onLeave(S,$){S._isLeaving=!0;const ae=()=>R(S,$);Ut(S,f),Wd(),Ut(S,d),cs(()=>{S._isLeaving&&(ln(S,f),Ut(S,h),ls(b)||us(S,r,E,ae))}),sn(b,[S,ae])},onEnterCancelled(S){M(S,!1),sn(v,[S])},onAppearCancelled(S){M(S,!0),sn(g,[S])},onLeaveCancelled(S){R(S),sn(P,[S])}})}function Bd(e){if(e==null)return null;if(Se(e))return[Qa(e.enter),Qa(e.leave)];{const t=Qa(e);return[t,t]}}function Qa(e){return Gu(e)}function Ut(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[vr]||(e[vr]=new Set)).add(t)}function ln(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const n=e[vr];n&&(n.delete(t),n.size||(e[vr]=void 0))}function cs(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Ud=0;function us(e,t,n,r){const a=e._endId=++Ud,o=()=>{a===e._endId&&r()};if(n)return setTimeout(o,n);const{type:i,timeout:s,propCount:l}=qd(e,t);if(!i)return r();const c=i+"end";let u=0;const f=()=>{e.removeEventListener(c,d),o()},d=h=>{h.target===e&&++u>=l&&f()};setTimeout(()=>{u(n[w]||"").split(", "),a=r(`${Bt}Delay`),o=r(`${Bt}Duration`),i=fs(a,o),s=r(`${Jn}Delay`),l=r(`${Jn}Duration`),c=fs(s,l);let u=null,f=0,d=0;t===Bt?i>0&&(u=Bt,f=i,d=o.length):t===Jn?c>0&&(u=Jn,f=c,d=l.length):(f=Math.max(i,c),u=f>0?i>c?Bt:Jn:null,d=u?u===Bt?o.length:l.length:0);const h=u===Bt&&/\b(transform|all)(,|$)/.test(r(`${Bt}Property`).toString());return{type:u,timeout:f,propCount:d,hasTransform:h}}function fs(e,t){for(;e.lengthds(n)+ds(e[r])))}function ds(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Wd(){return document.body.offsetHeight}function Kd(e,t,n){const r=e[vr];r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const ii=Symbol("_vod"),ya={beforeMount(e,{value:t},{transition:n}){e[ii]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):Qn(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),Qn(e,!0),r.enter(e)):r.leave(e,()=>{Qn(e,!1)}):Qn(e,t))},beforeUnmount(e,{value:t}){Qn(e,t)}};function Qn(e,t){e.style.display=t?e[ii]:"none"}function Vd(e,t,n){const r=e.style,a=ge(n);if(n&&!a){if(t&&!ge(t))for(const o in t)n[o]==null&&So(r,o,"");for(const o in n)So(r,o,n[o])}else{const o=r.display;a?t!==n&&(r.cssText=n):t&&e.removeAttribute("style"),ii in e&&(r.display=o)}}const ms=/\s*!important$/;function So(e,t,n){if(te(n))n.forEach(r=>So(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=Yd(e,t);ms.test(n)?e.setProperty(kn(r),n.replace(ms,""),"important"):e[r]=n}}const ps=["Webkit","Moz","ms"],Za={};function Yd(e,t){const n=Za[t];if(n)return n;let r=Et(t);if(r!=="filter"&&r in e)return Za[t]=r;r=Pa(r);for(let a=0;aeo||(tm.then(()=>eo=0),eo=Date.now());function rm(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;st(am(r,n.value),t,5,[r])};return n.value=e,n.attached=nm(),n}function am(e,t){if(te(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>a=>!a._stopped&&r&&r(a))}else return t}const bs=/^on[a-z]/,om=(e,t,n,r,a=!1,o,i,s,l)=>{t==="class"?Kd(e,r,a):t==="style"?Vd(e,n,r):Er(t)?Uo(t)||Zd(e,t,n,r,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):im(e,t,r,a))?Xd(e,t,r,o,i,s,l):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),Gd(e,t,r,a))};function im(e,t,n,r){return r?!!(t==="innerHTML"||t==="textContent"||t in e&&bs.test(t)&&ie(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||bs.test(t)&&ge(n)?!1:t in e}const sm={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},lm=(e,t)=>n=>{if(!("key"in n))return;const r=kn(n.key);if(t.some(a=>a===r||sm[a]===r))return e(n)},cm=Ne({patchProp:om},Fd);let to,ys=!1;function um(){return to=ys?to:yd(cm),ys=!0,to}const fm=(...e)=>{const t=um().createApp(...e),{mount:n}=t;return t.mount=r=>{const a=dm(r);if(a)return n(a,!0,a instanceof SVGElement)},t};function dm(e){return ge(e)?document.querySelector(e):e}const mm={"v-8daa1a0e":()=>He(()=>import("./index.html-a099ade6.js"),[]).then(({data:e})=>e),"v-7e348068":()=>He(()=>import("./testimonials.html-96ecbae2.js"),[]).then(({data:e})=>e),"v-fffb8e28":()=>He(()=>import("./index.html-2126a2d0.js"),[]).then(({data:e})=>e),"v-5c82f7c0":()=>He(()=>import("./index.html-0e290538.js"),[]).then(({data:e})=>e),"v-73866439":()=>He(()=>import("./index.html-20640c9f.js"),[]).then(({data:e})=>e),"v-3706649a":()=>He(()=>import("./404.html-60b35caa.js"),[]).then(({data:e})=>e)},pm=JSON.parse(`{"base":"/jmeter-java-dsl/","lang":"en-US","title":"jmeter-java-dsl","description":"Simple JMeter performance tests API","head":[["link",{"rel":"shortcut icon","href":"/jmeter-java-dsl/favicon.ico"}],["script",{},"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':\\n new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],\\n j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=\\n 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);\\n })(window,document,'script','dataLayer','GTM-PHSGKLD');\\n "]],"locales":{}}`);var hm=([e,t,n])=>e==="meta"&&t.name?`${e}.${t.name}`:["title","base"].includes(e)?e:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,t,n]),vm=e=>{const t=new Set,n=[];return e.forEach(r=>{const a=hm(r);t.has(a)||(t.add(a),n.push(r))}),n},Ir=e=>/^(https?:)?\/\//.test(e),gm=e=>/^mailto:/.test(e),bm=e=>/^tel:/.test(e),si=e=>Object.prototype.toString.call(e)==="[object Object]",xc=e=>e[e.length-1]==="/"?e.slice(0,-1):e,Ec=e=>e[0]==="/"?e.slice(1):e,Cc=(e,t)=>{const n=Object.keys(e).sort((r,a)=>{const o=a.split("/").length-r.split("/").length;return o!==0?o:a.length-r.length});for(const r of n)if(t.startsWith(r))return r;return"/"};const Sc={"v-8daa1a0e":it(()=>He(()=>import("./index.html-bb230a72.js"),[])),"v-7e348068":it(()=>He(()=>import("./testimonials.html-0b80892c.js"),[])),"v-fffb8e28":it(()=>He(()=>import("./index.html-8c2b6955.js"),[])),"v-5c82f7c0":it(()=>He(()=>import("./index.html-0b2e03e0.js"),[])),"v-73866439":it(()=>He(()=>import("./index.html-60f5385c.js"),[])),"v-3706649a":it(()=>He(()=>import("./404.html-c29c87b1.js"),[]))};var ym=Symbol(""),_m=ve(mm),Ac=Vn({key:"",path:"",title:"",lang:"",frontmatter:{},headers:[]}),Vt=ve(Ac),Jt=()=>Vt,Tc=Symbol(""),It=()=>{const e=Ie(Tc);if(!e)throw new Error("usePageFrontmatter() is called without provider.");return e},Lc=Symbol(""),wm=()=>{const e=Ie(Lc);if(!e)throw new Error("usePageHead() is called without provider.");return e},km=Symbol(""),Pc=Symbol(""),xm=()=>{const e=Ie(Pc);if(!e)throw new Error("usePageLang() is called without provider.");return e},Oc=Symbol(""),Em=()=>{const e=Ie(Oc);if(!e)throw new Error("usePageLayout() is called without provider.");return e},li=Symbol(""),Yn=()=>{const e=Ie(li);if(!e)throw new Error("useRouteLocale() is called without provider.");return e},Ln=ve(pm),Ic=()=>Ln,Rc=Symbol(""),ci=()=>{const e=Ie(Rc);if(!e)throw new Error("useSiteLocaleData() is called without provider.");return e},Cm=Symbol(""),Sm="Layout",Am="NotFound",At=Sr({resolveLayouts:e=>e.reduce((t,n)=>({...t,...n.layouts}),{}),resolvePageData:async e=>{const t=_m.value[e];return await(t==null?void 0:t())??Ac},resolvePageFrontmatter:e=>e.frontmatter,resolvePageHead:(e,t,n)=>{const r=ge(t.description)?t.description:n.description,a=[...te(t.head)?t.head:[],...n.head,["title",{},e],["meta",{name:"description",content:r}]];return vm(a)},resolvePageHeadTitle:(e,t)=>[e.title,t.title].filter(n=>!!n).join(" | "),resolvePageLang:e=>e.lang||"en",resolvePageLayout:(e,t)=>{let n;if(e.path){const r=e.frontmatter.layout;ge(r)?n=r:n=Sm}else n=Am;return t[n]},resolveRouteLocale:(e,t)=>Cc(e,t),resolveSiteLocaleData:(e,t)=>({...e,...e.locales[t]})}),Ha=fe({name:"ClientOnly",setup(e,t){const n=ve(!1);return Ye(()=>{n.value=!0}),()=>{var r,a;return n.value?(a=(r=t.slots).default)==null?void 0:a.call(r):null}}}),Tm=fe({name:"Content",props:{pageKey:{type:String,required:!1,default:""}},setup(e){const t=Jt(),n=D(()=>Sc[e.pageKey||t.value.key]);return()=>n.value?ue(n.value):ue("div","404 Not Found")}}),gt=(e={})=>e,ui=e=>Ir(e)?e:`/jmeter-java-dsl/${Ec(e)}`;function Nc(e,t,n){var r,a,o;t===void 0&&(t=50),n===void 0&&(n={});var i=(r=n.isImmediate)!=null&&r,s=(a=n.callback)!=null&&a,l=n.maxWait,c=Date.now(),u=[];function f(){if(l!==void 0){var h=Date.now()-c;if(h+t>=l)return l-h}return t}var d=function(){var h=[].slice.call(arguments),w=this;return new Promise(function(k,E){var _=i&&o===void 0;if(o!==void 0&&clearTimeout(o),o=setTimeout(function(){if(o=void 0,c=Date.now(),!i){var v=e.apply(w,h);s&&s(v),u.forEach(function(b){return(0,b.resolve)(v)}),u=[]}},f()),_){var x=e.apply(w,h);return s&&s(x),k(x)}u.push({resolve:k,reject:E})})};return d.cancel=function(h){o!==void 0&&clearTimeout(o),u.forEach(function(w){return(0,w.reject)(h)}),u=[]},d}/*! + * vue-router v4.2.5 + * (c) 2023 Eduardo San Martin Morote + * @license MIT + */const Tn=typeof window<"u";function Lm(e){return e.__esModule||e[Symbol.toStringTag]==="Module"}const be=Object.assign;function no(e,t){const n={};for(const r in t){const a=t[r];n[r]=vt(a)?a.map(e):e(a)}return n}const lr=()=>{},vt=Array.isArray,Pm=/\/$/,Om=e=>e.replace(Pm,"");function ro(e,t,n="/"){let r,a={},o="",i="";const s=t.indexOf("#");let l=t.indexOf("?");return s=0&&(l=-1),l>-1&&(r=t.slice(0,l),o=t.slice(l+1,s>-1?s:t.length),a=e(o)),s>-1&&(r=r||t.slice(0,s),i=t.slice(s,t.length)),r=Mm(r??t,n),{fullPath:r+(o&&"?")+o+i,path:r,query:a,hash:i}}function Im(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function _s(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Rm(e,t,n){const r=t.matched.length-1,a=n.matched.length-1;return r>-1&&r===a&&Bn(t.matched[r],n.matched[a])&&Mc(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Bn(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Mc(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!Nm(e[n],t[n]))return!1;return!0}function Nm(e,t){return vt(e)?ws(e,t):vt(t)?ws(t,e):e===t}function ws(e,t){return vt(t)?e.length===t.length&&e.every((n,r)=>n===t[r]):e.length===1&&e[0]===t}function Mm(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/"),a=r[r.length-1];(a===".."||a===".")&&r.push("");let o=n.length-1,i,s;for(i=0;i1&&o--;else break;return n.slice(0,o).join("/")+"/"+r.slice(i-(i===r.length?1:0)).join("/")}var gr;(function(e){e.pop="pop",e.push="push"})(gr||(gr={}));var cr;(function(e){e.back="back",e.forward="forward",e.unknown=""})(cr||(cr={}));function $m(e){if(!e)if(Tn){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),Om(e)}const zm=/^[^#]+#/;function Dm(e,t){return e.replace(zm,"#")+t}function Hm(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const Fa=()=>({left:window.pageXOffset,top:window.pageYOffset});function Fm(e){let t;if("el"in e){const n=e.el,r=typeof n=="string"&&n.startsWith("#"),a=typeof n=="string"?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!a)return;t=Hm(a,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.pageXOffset,t.top!=null?t.top:window.pageYOffset)}function ks(e,t){return(history.state?history.state.position-t:-1)+e}const Ao=new Map;function jm(e,t){Ao.set(e,t)}function Bm(e){const t=Ao.get(e);return Ao.delete(e),t}let Um=()=>location.protocol+"//"+location.host;function $c(e,t){const{pathname:n,search:r,hash:a}=t,o=e.indexOf("#");if(o>-1){let s=a.includes(e.slice(o))?e.slice(o).length:1,l=a.slice(s);return l[0]!=="/"&&(l="/"+l),_s(l,"")}return _s(n,e)+r+a}function qm(e,t,n,r){let a=[],o=[],i=null;const s=({state:d})=>{const h=$c(e,location),w=n.value,k=t.value;let E=0;if(d){if(n.value=h,t.value=d,i&&i===w){i=null;return}E=k?d.position-k.position:0}else r(h);a.forEach(_=>{_(n.value,w,{delta:E,type:gr.pop,direction:E?E>0?cr.forward:cr.back:cr.unknown})})};function l(){i=n.value}function c(d){a.push(d);const h=()=>{const w=a.indexOf(d);w>-1&&a.splice(w,1)};return o.push(h),h}function u(){const{history:d}=window;d.state&&d.replaceState(be({},d.state,{scroll:Fa()}),"")}function f(){for(const d of o)d();o=[],window.removeEventListener("popstate",s),window.removeEventListener("beforeunload",u)}return window.addEventListener("popstate",s),window.addEventListener("beforeunload",u,{passive:!0}),{pauseListeners:l,listen:c,destroy:f}}function xs(e,t,n,r=!1,a=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:a?Fa():null}}function Wm(e){const{history:t,location:n}=window,r={value:$c(e,n)},a={value:t.state};a.value||o(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function o(l,c,u){const f=e.indexOf("#"),d=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+l:Um()+e+l;try{t[u?"replaceState":"pushState"](c,"",d),a.value=c}catch(h){console.error(h),n[u?"replace":"assign"](d)}}function i(l,c){const u=be({},t.state,xs(a.value.back,l,a.value.forward,!0),c,{position:a.value.position});o(l,u,!0),r.value=l}function s(l,c){const u=be({},a.value,t.state,{forward:l,scroll:Fa()});o(u.current,u,!0);const f=be({},xs(r.value,l,null),{position:u.position+1},c);o(l,f,!1),r.value=l}return{location:r,state:a,push:s,replace:i}}function Km(e){e=$m(e);const t=Wm(e),n=qm(e,t.state,t.location,t.replace);function r(o,i=!0){i||n.pauseListeners(),history.go(o)}const a=be({location:"",base:e,go:r,createHref:Dm.bind(null,e)},t,n);return Object.defineProperty(a,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(a,"state",{enumerable:!0,get:()=>t.state.value}),a}function Vm(e){return typeof e=="string"||e&&typeof e=="object"}function zc(e){return typeof e=="string"||typeof e=="symbol"}const Tt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},Dc=Symbol("");var Es;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(Es||(Es={}));function Un(e,t){return be(new Error,{type:e,[Dc]:!0},t)}function St(e,t){return e instanceof Error&&Dc in e&&(t==null||!!(e.type&t))}const Cs="[^/]+?",Ym={sensitive:!1,strict:!1,start:!0,end:!0},Gm=/[.+*?^${}()[\]/\\]/g;function Xm(e,t){const n=be({},Ym,t),r=[];let a=n.start?"^":"";const o=[];for(const c of e){const u=c.length?[]:[90];n.strict&&!c.length&&(a+="/");for(let f=0;ft.length?t.length===1&&t[0]===40+40?1:-1:0}function Qm(e,t){let n=0;const r=e.score,a=t.score;for(;n0&&t[t.length-1]<0}const Zm={type:0,value:""},ep=/[a-zA-Z0-9_]/;function tp(e){if(!e)return[[]];if(e==="/")return[[Zm]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(h){throw new Error(`ERR (${n})/"${c}": ${h}`)}let n=0,r=n;const a=[];let o;function i(){o&&a.push(o),o=[]}let s=0,l,c="",u="";function f(){c&&(n===0?o.push({type:0,value:c}):n===1||n===2||n===3?(o.length>1&&(l==="*"||l==="+")&&t(`A repeatable param (${c}) must be alone in its segment. eg: '/:ids+.`),o.push({type:1,value:c,regexp:u,repeatable:l==="*"||l==="+",optional:l==="*"||l==="?"})):t("Invalid state to consume buffer"),c="")}function d(){c+=l}for(;s{i(x)}:lr}function i(u){if(zc(u)){const f=r.get(u);f&&(r.delete(u),n.splice(n.indexOf(f),1),f.children.forEach(i),f.alias.forEach(i))}else{const f=n.indexOf(u);f>-1&&(n.splice(f,1),u.record.name&&r.delete(u.record.name),u.children.forEach(i),u.alias.forEach(i))}}function s(){return n}function l(u){let f=0;for(;f=0&&(u.record.path!==n[f].record.path||!Hc(u,n[f]));)f++;n.splice(f,0,u),u.record.name&&!Ts(u)&&r.set(u.record.name,u)}function c(u,f){let d,h={},w,k;if("name"in u&&u.name){if(d=r.get(u.name),!d)throw Un(1,{location:u});k=d.record.name,h=be(As(f.params,d.keys.filter(x=>!x.optional).map(x=>x.name)),u.params&&As(u.params,d.keys.map(x=>x.name))),w=d.stringify(h)}else if("path"in u)w=u.path,d=n.find(x=>x.re.test(w)),d&&(h=d.parse(w),k=d.record.name);else{if(d=f.name?r.get(f.name):n.find(x=>x.re.test(f.path)),!d)throw Un(1,{location:u,currentLocation:f});k=d.record.name,h=be({},f.params,u.params),w=d.stringify(h)}const E=[];let _=d;for(;_;)E.unshift(_.record),_=_.parent;return{name:k,path:w,params:h,matched:E,meta:ip(E)}}return e.forEach(u=>o(u)),{addRoute:o,resolve:c,removeRoute:i,getRoutes:s,getRecordMatcher:a}}function As(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function ap(e){return{path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:void 0,beforeEnter:e.beforeEnter,props:op(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}}}function op(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]=typeof n=="object"?n[r]:n;return t}function Ts(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function ip(e){return e.reduce((t,n)=>be(t,n.meta),{})}function Ls(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}function Hc(e,t){return t.children.some(n=>n===e||Hc(e,n))}const Fc=/#/g,sp=/&/g,lp=/\//g,cp=/=/g,up=/\?/g,jc=/\+/g,fp=/%5B/g,dp=/%5D/g,Bc=/%5E/g,mp=/%60/g,Uc=/%7B/g,pp=/%7C/g,qc=/%7D/g,hp=/%20/g;function fi(e){return encodeURI(""+e).replace(pp,"|").replace(fp,"[").replace(dp,"]")}function vp(e){return fi(e).replace(Uc,"{").replace(qc,"}").replace(Bc,"^")}function To(e){return fi(e).replace(jc,"%2B").replace(hp,"+").replace(Fc,"%23").replace(sp,"%26").replace(mp,"`").replace(Uc,"{").replace(qc,"}").replace(Bc,"^")}function gp(e){return To(e).replace(cp,"%3D")}function bp(e){return fi(e).replace(Fc,"%23").replace(up,"%3F")}function yp(e){return e==null?"":bp(e).replace(lp,"%2F")}function _a(e){try{return decodeURIComponent(""+e)}catch{}return""+e}function _p(e){const t={};if(e===""||e==="?")return t;const r=(e[0]==="?"?e.slice(1):e).split("&");for(let a=0;ao&&To(o)):[r&&To(r)]).forEach(o=>{o!==void 0&&(t+=(t.length?"&":"")+n,o!=null&&(t+="="+o))})}return t}function wp(e){const t={};for(const n in e){const r=e[n];r!==void 0&&(t[n]=vt(r)?r.map(a=>a==null?null:""+a):r==null?r:""+r)}return t}const kp=Symbol(""),Os=Symbol(""),ja=Symbol(""),di=Symbol(""),Lo=Symbol("");function Zn(){let e=[];function t(r){return e.push(r),()=>{const a=e.indexOf(r);a>-1&&e.splice(a,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function Yt(e,t,n,r,a){const o=r&&(r.enterCallbacks[a]=r.enterCallbacks[a]||[]);return()=>new Promise((i,s)=>{const l=f=>{f===!1?s(Un(4,{from:n,to:t})):f instanceof Error?s(f):Vm(f)?s(Un(2,{from:t,to:f})):(o&&r.enterCallbacks[a]===o&&typeof f=="function"&&o.push(f),i())},c=e.call(r&&r.instances[a],t,n,l);let u=Promise.resolve(c);e.length<3&&(u=u.then(l)),u.catch(f=>s(f))})}function ao(e,t,n,r){const a=[];for(const o of e)for(const i in o.components){let s=o.components[i];if(!(t!=="beforeRouteEnter"&&!o.instances[i]))if(xp(s)){const c=(s.__vccOpts||s)[t];c&&a.push(Yt(c,n,r,o,i))}else{let l=s();a.push(()=>l.then(c=>{if(!c)return Promise.reject(new Error(`Couldn't resolve component "${i}" at "${o.path}"`));const u=Lm(c)?c.default:c;o.components[i]=u;const d=(u.__vccOpts||u)[t];return d&&Yt(d,n,r,o,i)()}))}}return a}function xp(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function Is(e){const t=Ie(ja),n=Ie(di),r=D(()=>t.resolve(Q(e.to))),a=D(()=>{const{matched:l}=r.value,{length:c}=l,u=l[c-1],f=n.matched;if(!u||!f.length)return-1;const d=f.findIndex(Bn.bind(null,u));if(d>-1)return d;const h=Rs(l[c-2]);return c>1&&Rs(u)===h&&f[f.length-1].path!==h?f.findIndex(Bn.bind(null,l[c-2])):d}),o=D(()=>a.value>-1&&Ap(n.params,r.value.params)),i=D(()=>a.value>-1&&a.value===n.matched.length-1&&Mc(n.params,r.value.params));function s(l={}){return Sp(l)?t[Q(e.replace)?"replace":"push"](Q(e.to)).catch(lr):Promise.resolve()}return{route:r,href:D(()=>r.value.href),isActive:o,isExactActive:i,navigate:s}}const Ep=fe({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:Is,setup(e,{slots:t}){const n=Sr(Is(e)),{options:r}=Ie(ja),a=D(()=>({[Ns(e.activeClass,r.linkActiveClass,"router-link-active")]:n.isActive,[Ns(e.exactActiveClass,r.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const o=t.default&&t.default(n);return e.custom?o:ue("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:a.value},o)}}}),Cp=Ep;function Sp(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Ap(e,t){for(const n in t){const r=t[n],a=e[n];if(typeof r=="string"){if(r!==a)return!1}else if(!vt(a)||a.length!==r.length||r.some((o,i)=>o!==a[i]))return!1}return!0}function Rs(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Ns=(e,t,n)=>e??t??n,Tp=fe({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const r=Ie(Lo),a=D(()=>e.route||r.value),o=Ie(Os,0),i=D(()=>{let c=Q(o);const{matched:u}=a.value;let f;for(;(f=u[c])&&!f.components;)c++;return c}),s=D(()=>a.value.matched[i.value]);gn(Os,D(()=>i.value+1)),gn(kp,s),gn(Lo,a);const l=ve();return lt(()=>[l.value,s.value,e.name],([c,u,f],[d,h,w])=>{u&&(u.instances[f]=c,h&&h!==u&&c&&c===d&&(u.leaveGuards.size||(u.leaveGuards=h.leaveGuards),u.updateGuards.size||(u.updateGuards=h.updateGuards))),c&&u&&(!h||!Bn(u,h)||!d)&&(u.enterCallbacks[f]||[]).forEach(k=>k(c))},{flush:"post"}),()=>{const c=a.value,u=e.name,f=s.value,d=f&&f.components[u];if(!d)return Ms(n.default,{Component:d,route:c});const h=f.props[u],w=h?h===!0?c.params:typeof h=="function"?h(c):h:null,E=ue(d,be({},w,t,{onVnodeUnmounted:_=>{_.component.isUnmounted&&(f.instances[u]=null)},ref:l}));return Ms(n.default,{Component:E,route:c})||E}}});function Ms(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Wc=Tp;function Lp(e){const t=rp(e.routes,e),n=e.parseQuery||_p,r=e.stringifyQuery||Ps,a=e.history,o=Zn(),i=Zn(),s=Zn(),l=Qo(Tt);let c=Tt;Tn&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const u=no.bind(null,A=>""+A),f=no.bind(null,yp),d=no.bind(null,_a);function h(A,U){let H,X;return zc(A)?(H=t.getRecordMatcher(A),X=U):X=A,t.addRoute(X,H)}function w(A){const U=t.getRecordMatcher(A);U&&t.removeRoute(U)}function k(){return t.getRoutes().map(A=>A.record)}function E(A){return!!t.getRecordMatcher(A)}function _(A,U){if(U=be({},U||l.value),typeof A=="string"){const y=ro(n,A,U.path),C=t.resolve({path:y.path},U),T=a.createHref(y.fullPath);return be(y,C,{params:d(C.params),hash:_a(y.hash),redirectedFrom:void 0,href:T})}let H;if("path"in A)H=be({},A,{path:ro(n,A.path,U.path).path});else{const y=be({},A.params);for(const C in y)y[C]==null&&delete y[C];H=be({},A,{params:f(y)}),U.params=f(U.params)}const X=t.resolve(H,U),de=A.hash||"";X.params=u(d(X.params));const m=Im(r,be({},A,{hash:vp(de),path:X.path})),p=a.createHref(m);return be({fullPath:m,hash:de,query:r===Ps?wp(A.query):A.query||{}},X,{redirectedFrom:void 0,href:p})}function x(A){return typeof A=="string"?ro(n,A,l.value.path):be({},A)}function v(A,U){if(c!==A)return Un(8,{from:U,to:A})}function b(A){return I(A)}function P(A){return b(be(x(A),{replace:!0}))}function B(A){const U=A.matched[A.matched.length-1];if(U&&U.redirect){const{redirect:H}=U;let X=typeof H=="function"?H(A):H;return typeof X=="string"&&(X=X.includes("?")||X.includes("#")?X=x(X):{path:X},X.params={}),be({query:A.query,hash:A.hash,params:"path"in X?{}:A.params},X)}}function I(A,U){const H=c=_(A),X=l.value,de=A.state,m=A.force,p=A.replace===!0,y=B(H);if(y)return I(be(x(y),{state:typeof y=="object"?be({},de,y.state):de,force:m,replace:p}),U||H);const C=H;C.redirectedFrom=U;let T;return!m&&Rm(r,X,H)&&(T=Un(16,{to:C,from:X}),Ge(X,X,!0,!1)),(T?Promise.resolve(T):R(C,X)).catch(L=>St(L)?St(L,2)?L:je(L):W(L,C,X)).then(L=>{if(L){if(St(L,2))return I(be({replace:p},x(L.to),{state:typeof L.to=="object"?be({},de,L.to.state):de,force:m}),U||C)}else L=S(C,X,!0,p,de);return J(C,X,L),L})}function g(A,U){const H=v(A,U);return H?Promise.reject(H):Promise.resolve()}function M(A){const U=Ht.values().next().value;return U&&typeof U.runWithContext=="function"?U.runWithContext(A):A()}function R(A,U){let H;const[X,de,m]=Pp(A,U);H=ao(X.reverse(),"beforeRouteLeave",A,U);for(const y of X)y.leaveGuards.forEach(C=>{H.push(Yt(C,A,U))});const p=g.bind(null,A,U);return H.push(p),ze(H).then(()=>{H=[];for(const y of o.list())H.push(Yt(y,A,U));return H.push(p),ze(H)}).then(()=>{H=ao(de,"beforeRouteUpdate",A,U);for(const y of de)y.updateGuards.forEach(C=>{H.push(Yt(C,A,U))});return H.push(p),ze(H)}).then(()=>{H=[];for(const y of m)if(y.beforeEnter)if(vt(y.beforeEnter))for(const C of y.beforeEnter)H.push(Yt(C,A,U));else H.push(Yt(y.beforeEnter,A,U));return H.push(p),ze(H)}).then(()=>(A.matched.forEach(y=>y.enterCallbacks={}),H=ao(m,"beforeRouteEnter",A,U),H.push(p),ze(H))).then(()=>{H=[];for(const y of i.list())H.push(Yt(y,A,U));return H.push(p),ze(H)}).catch(y=>St(y,8)?y:Promise.reject(y))}function J(A,U,H){s.list().forEach(X=>M(()=>X(A,U,H)))}function S(A,U,H,X,de){const m=v(A,U);if(m)return m;const p=U===Tt,y=Tn?history.state:{};H&&(X||p?a.replace(A.fullPath,be({scroll:p&&y&&y.scroll},de)):a.push(A.fullPath,de)),l.value=A,Ge(A,U,H,p),je()}let $;function ae(){$||($=a.listen((A,U,H)=>{if(!bt.listening)return;const X=_(A),de=B(X);if(de){I(be(de,{replace:!0}),X).catch(lr);return}c=X;const m=l.value;Tn&&jm(ks(m.fullPath,H.delta),Fa()),R(X,m).catch(p=>St(p,12)?p:St(p,2)?(I(p.to,X).then(y=>{St(y,20)&&!H.delta&&H.type===gr.pop&&a.go(-1,!1)}).catch(lr),Promise.reject()):(H.delta&&a.go(-H.delta,!1),W(p,X,m))).then(p=>{p=p||S(X,m,!1),p&&(H.delta&&!St(p,8)?a.go(-H.delta,!1):H.type===gr.pop&&St(p,20)&&a.go(-1,!1)),J(X,m,p)}).catch(lr)}))}let se=Zn(),N=Zn(),G;function W(A,U,H){je(A);const X=N.list();return X.length?X.forEach(de=>de(A,U,H)):console.error(A),Promise.reject(A)}function $e(){return G&&l.value!==Tt?Promise.resolve():new Promise((A,U)=>{se.add([A,U])})}function je(A){return G||(G=!A,ae(),se.list().forEach(([U,H])=>A?H(A):U()),se.reset()),A}function Ge(A,U,H,X){const{scrollBehavior:de}=e;if(!Tn||!de)return Promise.resolve();const m=!H&&Bm(ks(A.fullPath,0))||(X||!H)&&history.state&&history.state.scroll||null;return Tr().then(()=>de(A,U,m)).then(p=>p&&Fm(p)).catch(p=>W(p,A,U))}const Ue=A=>a.go(A);let Dt;const Ht=new Set,bt={currentRoute:l,listening:!0,addRoute:h,removeRoute:w,hasRoute:E,getRoutes:k,resolve:_,options:e,push:b,replace:P,go:Ue,back:()=>Ue(-1),forward:()=>Ue(1),beforeEach:o.add,beforeResolve:i.add,afterEach:s.add,onError:N.add,isReady:$e,install(A){const U=this;A.component("RouterLink",Cp),A.component("RouterView",Wc),A.config.globalProperties.$router=U,Object.defineProperty(A.config.globalProperties,"$route",{enumerable:!0,get:()=>Q(l)}),Tn&&!Dt&&l.value===Tt&&(Dt=!0,b(a.location).catch(de=>{}));const H={};for(const de in Tt)Object.defineProperty(H,de,{get:()=>l.value[de],enumerable:!0});A.provide(ja,U),A.provide(di,jl(H)),A.provide(Lo,l);const X=A.unmount;Ht.add(A),A.unmount=function(){Ht.delete(A),Ht.size<1&&(c=Tt,$&&$(),$=null,l.value=Tt,Dt=!1,G=!1),X()}}};function ze(A){return A.reduce((U,H)=>U.then(()=>M(H)),Promise.resolve())}return bt}function Pp(e,t){const n=[],r=[],a=[],o=Math.max(t.matched.length,e.matched.length);for(let i=0;iBn(c,s))?r.push(s):n.push(s));const l=e.matched[i];l&&(t.matched.find(c=>Bn(c,l))||a.push(l))}return[n,r,a]}function xn(){return Ie(ja)}function En(){return Ie(di)}const Op=({headerLinkSelector:e,headerAnchorSelector:t,delay:n,offset:r=5})=>{const a=xn(),i=Nc(()=>{var k,E;const s=Math.max(window.scrollY,document.documentElement.scrollTop,document.body.scrollTop);if(Math.abs(s-0)d.some(x=>x.hash===_.hash));for(let _=0;_=(((k=x.parentElement)==null?void 0:k.offsetTop)??0)-r,P=!v||s<(((E=v.parentElement)==null?void 0:E.offsetTop)??0)-r;if(!(b&&P))continue;const I=decodeURIComponent(a.currentRoute.value.hash),g=decodeURIComponent(x.hash);if(I===g)return;if(f){for(let M=_+1;M{window.addEventListener("scroll",i)}),Pr(()=>{window.removeEventListener("scroll",i)})},$s=async(e,t)=>{const{scrollBehavior:n}=e.options;e.options.scrollBehavior=void 0,await e.replace({query:e.currentRoute.value.query,hash:t}).finally(()=>e.options.scrollBehavior=n)},Ip="a.sidebar-item",Rp=".header-anchor",Np=300,Mp=5,$p=gt({setup(){Op({headerLinkSelector:Ip,headerAnchorSelector:Rp,delay:Np,offset:Mp})}}),zs=()=>window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,zp=()=>window.scrollTo({top:0,behavior:"smooth"});const Dp=fe({name:"BackToTop",setup(){const e=ve(0),t=D(()=>e.value>300),n=Nc(()=>{e.value=zs()},100);Ye(()=>{e.value=zs(),window.addEventListener("scroll",()=>n())});const r=ue("div",{class:"back-to-top",onClick:zp});return()=>ue(Or,{name:"back-to-top"},()=>t.value?r:null)}}),Hp=gt({rootComponents:[Dp]});const Fp=ue("svg",{class:"external-link-icon",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",x:"0px",y:"0px",viewBox:"0 0 100 100",width:"15",height:"15"},[ue("path",{fill:"currentColor",d:"M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"}),ue("polygon",{fill:"currentColor",points:"45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"})]),jp=fe({name:"ExternalLinkIcon",props:{locales:{type:Object,required:!1,default:()=>({})}},setup(e){const t=Yn(),n=D(()=>e.locales[t.value]??{openInNewWindow:"open in new window"});return()=>ue("span",[Fp,ue("span",{class:"external-link-icon-sr-only"},n.value.openInNewWindow)])}}),Bp={"/":{openInNewWindow:"open in new window"}},Up=gt({enhance({app:e}){e.component("ExternalLinkIcon",ue(jp,{locales:Bp}))}});/** + * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT + */const me={settings:{minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
'},status:null,set:e=>{const t=me.isStarted();e=oo(e,me.settings.minimum,1),me.status=e===1?null:e;const n=me.render(!t),r=n.querySelector(me.settings.barSelector),a=me.settings.speed,o=me.settings.easing;return n.offsetWidth,qp(i=>{Wr(r,{transform:"translate3d("+Ds(e)+"%,0,0)",transition:"all "+a+"ms "+o}),e===1?(Wr(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(function(){Wr(n,{transition:"all "+a+"ms linear",opacity:"0"}),setTimeout(function(){me.remove(),i()},a)},a)):setTimeout(()=>i(),a)}),me},isStarted:()=>typeof me.status=="number",start:()=>{me.status||me.set(0);const e=()=>{setTimeout(()=>{me.status&&(me.trickle(),e())},me.settings.trickleSpeed)};return me.settings.trickle&&e(),me},done:e=>!e&&!me.status?me:me.inc(.3+.5*Math.random()).set(1),inc:e=>{let t=me.status;return t?(typeof e!="number"&&(e=(1-t)*oo(Math.random()*t,.1,.95)),t=oo(t+e,0,.994),me.set(t)):me.start()},trickle:()=>me.inc(Math.random()*me.settings.trickleRate),render:e=>{if(me.isRendered())return document.getElementById("nprogress");Hs(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=me.settings.template;const n=t.querySelector(me.settings.barSelector),r=e?"-100":Ds(me.status||0),a=document.querySelector(me.settings.parent);return Wr(n,{transition:"all 0 linear",transform:"translate3d("+r+"%,0,0)"}),a!==document.body&&Hs(a,"nprogress-custom-parent"),a==null||a.appendChild(t),t},remove:()=>{Fs(document.documentElement,"nprogress-busy"),Fs(document.querySelector(me.settings.parent),"nprogress-custom-parent");const e=document.getElementById("nprogress");e&&Wp(e)},isRendered:()=>!!document.getElementById("nprogress")},oo=(e,t,n)=>en?n:e,Ds=e=>(-1+e)*100,qp=function(){const e=[];function t(){const n=e.shift();n&&n(t)}return function(n){e.push(n),e.length===1&&t()}}(),Wr=function(){const e=["Webkit","O","Moz","ms"],t={};function n(i){return i.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(s,l){return l.toUpperCase()})}function r(i){const s=document.body.style;if(i in s)return i;let l=e.length;const c=i.charAt(0).toUpperCase()+i.slice(1);let u;for(;l--;)if(u=e[l]+c,u in s)return u;return i}function a(i){return i=n(i),t[i]??(t[i]=r(i))}function o(i,s,l){s=a(s),i.style[s]=l}return function(i,s){for(const l in s){const c=s[l];c!==void 0&&Object.prototype.hasOwnProperty.call(s,l)&&o(i,l,c)}}}(),Kc=(e,t)=>(typeof e=="string"?e:mi(e)).indexOf(" "+t+" ")>=0,Hs=(e,t)=>{const n=mi(e),r=n+t;Kc(n,t)||(e.className=r.substring(1))},Fs=(e,t)=>{const n=mi(e);if(!Kc(e,t))return;const r=n.replace(" "+t+" "," ");e.className=r.substring(1,r.length-1)},mi=e=>(" "+(e.className||"")+" ").replace(/\s+/gi," "),Wp=e=>{e&&e.parentNode&&e.parentNode.removeChild(e)};const Kp=()=>{Ye(()=>{const e=xn(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||me.start()}),e.afterEach(n=>{t.add(n.path),me.done()})})},Vp=gt({setup(){Kp()}}),Yp=JSON.parse(`{"logo":"/logo.svg","editLink":false,"lastUpdated":false,"contributors":false,"navbar":[{"text":"Guide","link":"/guide/"},{"text":"Support","link":"/support/"},{"text":"Motivation","link":"/motivation/"},{"text":"","link":"https://discord.gg/WNSn5hqmSd","icon":["fab","discord"]},{"text":"","link":"https://github.com/abstracta/jmeter-java-dsl","icon":["fab","github"]}],"sidebarDepth":3,"locales":{"/":{"selectLanguageName":"English"}},"colorMode":"auto","colorModeSwitch":true,"repo":null,"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","sidebar":"auto","editLinkText":"Edit this page","lastUpdatedText":"Last Updated","contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"backToHome":"Take me home","openInNewWindow":"open in new window","toggleColorMode":"toggle color mode","toggleSidebar":"toggle sidebar"}`),Gp=ve(Yp),Vc=()=>Gp,Yc=Symbol(""),Xp=()=>{const e=Ie(Yc);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},Jp=(e,t)=>{const{locales:n,...r}=e;return{...r,...n==null?void 0:n[t]}},Qp=gt({enhance({app:e}){const t=Vc(),n=e._context.provides[li],r=D(()=>Jp(t.value,n.value));e.provide(Yc,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}})}}),Zp=fe({__name:"Badge",props:{type:{type:String,required:!1,default:"tip"},text:{type:String,required:!1,default:""},vertical:{type:String,required:!1,default:void 0}},setup(e){return(t,n)=>(q(),ne("span",{class:Qe(["badge",e.type]),style:Cr({verticalAlign:e.vertical})},[ke(t.$slots,"default",{},()=>[Ct(Ke(e.text),1)])],6))}}),Ae=(e,t)=>{const n=e.__vccOpts||e;for(const[r,a]of t)n[r]=a;return n},eh=Ae(Zp,[["__file","Badge.vue"]]),th=fe({name:"CodeGroup",slots:Object,setup(e,{slots:t}){const n=ve(-1),r=ve([]),a=(s=n.value)=>{s{s>0?n.value=s-1:n.value=r.value.length-1,r.value[n.value].focus()},i=(s,l)=>{s.key===" "||s.key==="Enter"?(s.preventDefault(),n.value=l):s.key==="ArrowRight"?(s.preventDefault(),a(l)):s.key==="ArrowLeft"&&(s.preventDefault(),o(l))};return()=>{var l;const s=(((l=t.default)==null?void 0:l.call(t))||[]).filter(c=>c.type.name==="CodeGroupItem").map(c=>(c.props===null&&(c.props={}),c));return s.length===0?null:(n.value<0||n.value>s.length-1?(n.value=s.findIndex(c=>c.props.active===""||c.props.active===!0),n.value===-1&&(n.value=0)):s.forEach((c,u)=>{c.props.active=u===n.value}),ue("div",{class:"code-group"},[ue("div",{class:"code-group__nav"},ue("ul",{class:"code-group__ul"},s.map((c,u)=>{const f=u===n.value;return ue("li",{class:"code-group__li"},ue("button",{ref:d=>{d&&(r.value[u]=d)},class:{"code-group__nav-tab":!0,"code-group__nav-tab-active":f},ariaPressed:f,ariaExpanded:f,onClick:()=>n.value=u,onKeydown:d=>i(d,u)},c.props.title))}))),s]))}}}),nh=["aria-selected"],rh=fe({name:"CodeGroupItem"}),ah=fe({...rh,props:{title:{type:String,required:!0},active:{type:Boolean,required:!1,default:!1}},setup(e){return(t,n)=>(q(),ne("div",{class:Qe(["code-group-item",{"code-group-item__active":e.active}]),"aria-selected":e.active},[ke(t.$slots,"default")],10,nh))}}),oh=Ae(ah,[["__file","CodeGroupItem.vue"]]);function js(e,t){var n;const r=Qo();return ec(()=>{r.value=e()},{...t,flush:(n=t==null?void 0:t.flush)!=null?n:"sync"}),Vn(r)}function pi(e){return Pl()?(af(e),!0):!1}function tn(e){return typeof e=="function"?e():Q(e)}const hi=typeof window<"u"&&typeof document<"u",ih=Object.prototype.toString,sh=e=>ih.call(e)==="[object Object]",lh=()=>{};function ch(e,t){function n(...r){return new Promise((a,o)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(a).catch(o)})}return n}const Gc=e=>e();function uh(e=Gc){const t=ve(!0);function n(){t.value=!1}function r(){t.value=!0}const a=(...o)=>{t.value&&e(...o)};return{isActive:Vn(t),pause:n,resume:r,eventFilter:a}}function fh(e,t,n={}){const{eventFilter:r=Gc,...a}=n;return lt(e,ch(r,t),a)}function dh(e,t,n={}){const{eventFilter:r,...a}=n,{eventFilter:o,pause:i,resume:s,isActive:l}=uh(r);return{stop:fh(e,t,{...a,eventFilter:o}),pause:i,resume:s,isActive:l}}function mh(e,t,n={}){const{immediate:r=!0}=n,a=ve(!1);let o=null;function i(){o&&(clearTimeout(o),o=null)}function s(){a.value=!1,i()}function l(...c){i(),a.value=!0,o=setTimeout(()=>{a.value=!1,o=null,e(...c)},tn(t))}return r&&(a.value=!0,hi&&l()),pi(s),{isPending:Vn(a),start:l,stop:s}}function ph(e=!1,t={}){const{truthyValue:n=!0,falsyValue:r=!1}=t,a=Fe(e),o=ve(e);function i(s){if(arguments.length)return o.value=s,o.value;{const l=tn(n);return o.value=o.value===l?tn(r):l,o.value}}return a?i:[o,i]}function hh(e){var t;const n=tn(e);return(t=n==null?void 0:n.$el)!=null?t:n}const wa=hi?window:void 0,vh=hi?window.navigator:void 0;function ka(...e){let t,n,r,a;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,r,a]=e,t=wa):[t,n,r,a]=e,!t)return lh;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const o=[],i=()=>{o.forEach(u=>u()),o.length=0},s=(u,f,d,h)=>(u.addEventListener(f,d,h),()=>u.removeEventListener(f,d,h)),l=lt(()=>[hh(t),tn(a)],([u,f])=>{if(i(),!u)return;const d=sh(f)?{...f}:f;o.push(...n.flatMap(h=>r.map(w=>s(u,h,w,d))))},{immediate:!0,flush:"post"}),c=()=>{l(),i()};return pi(c),c}function gh(){const e=ve(!1);return ai()&&Ye(()=>{e.value=!0}),e}function Xc(e){const t=gh();return D(()=>(t.value,!!e()))}function bh(e,t={}){const{window:n=wa}=t,r=Xc(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let a;const o=ve(!1),i=c=>{o.value=c.matches},s=()=>{a&&("removeEventListener"in a?a.removeEventListener("change",i):a.removeListener(i))},l=ec(()=>{r.value&&(s(),a=n.matchMedia(tn(e)),"addEventListener"in a?a.addEventListener("change",i):a.addListener(i),o.value=a.matches)});return pi(()=>{l(),s(),a=void 0}),o}function yh(e={}){const{navigator:t=vh,read:n=!1,source:r,copiedDuring:a=1500,legacy:o=!1}=e,i=Xc(()=>t&&"clipboard"in t),s=D(()=>i.value||o),l=ve(""),c=ve(!1),u=mh(()=>c.value=!1,a);function f(){i.value?t.clipboard.readText().then(k=>{l.value=k}):l.value=w()}s.value&&n&&ka(["copy","cut"],f);async function d(k=tn(r)){s.value&&k!=null&&(i.value?await t.clipboard.writeText(k):h(k),l.value=k,c.value=!0,u.start())}function h(k){const E=document.createElement("textarea");E.value=k??"",E.style.position="absolute",E.style.opacity="0",document.body.appendChild(E),E.select(),document.execCommand("copy"),E.remove()}function w(){var k,E,_;return(_=(E=(k=document==null?void 0:document.getSelection)==null?void 0:k.call(document))==null?void 0:E.toString())!=null?_:""}return{isSupported:s,text:l,copied:c,copy:d}}const Kr=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},Vr="__vueuse_ssr_handlers__",_h=wh();function wh(){return Vr in Kr||(Kr[Vr]=Kr[Vr]||{}),Kr[Vr]}function kh(e,t){return _h[e]||t}function xh(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const Eh={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},Bs="vueuse-storage";function Ch(e,t,n,r={}){var a;const{flush:o="pre",deep:i=!0,listenToStorageChanges:s=!0,writeDefaults:l=!0,mergeDefaults:c=!1,shallow:u,window:f=wa,eventFilter:d,onError:h=g=>{console.error(g)}}=r,w=(u?Qo:ve)(t);if(!n)try{n=kh("getDefaultStorage",()=>{var g;return(g=wa)==null?void 0:g.localStorage})()}catch(g){h(g)}if(!n)return w;const k=tn(t),E=xh(k),_=(a=r.serializer)!=null?a:Eh[E],{pause:x,resume:v}=dh(w,()=>b(w.value),{flush:o,deep:i,eventFilter:d});return f&&s&&(ka(f,"storage",I),ka(f,Bs,B)),I(),w;function b(g){try{if(g==null)n.removeItem(e);else{const M=_.write(g),R=n.getItem(e);R!==M&&(n.setItem(e,M),f&&f.dispatchEvent(new CustomEvent(Bs,{detail:{key:e,oldValue:R,newValue:M,storageArea:n}})))}}catch(M){h(M)}}function P(g){const M=g?g.newValue:n.getItem(e);if(M==null)return l&&k!==null&&n.setItem(e,_.write(k)),k;if(!g&&c){const R=_.read(M);return typeof c=="function"?c(R,k):E==="object"&&!Array.isArray(R)?{...k,...R}:R}else return typeof M!="string"?M:_.read(M)}function B(g){I(g.detail)}function I(g){if(!(g&&g.storageArea!==n)){if(g&&g.key==null){w.value=k;return}if(!(g&&g.key!==e)){x();try{(g==null?void 0:g.newValue)!==_.write(w.value)&&(w.value=P(g))}catch(M){h(M)}finally{g?Tr(v):v()}}}}}function Sh(e){return bh("(prefers-color-scheme: dark)",e)}const Ah=()=>Vc(),Ve=()=>Xp(),Jc=Symbol(""),vi=()=>{const e=Ie(Jc);if(!e)throw new Error("useDarkMode() is called without provider.");return e},Th=()=>{const e=Ve(),t=Sh(),n=Ch("vuepress-color-scheme",e.value.colorMode),r=D({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(a){a===t.value?n.value="auto":n.value=a?"dark":"light"}});gn(Jc,r),Lh(r)},Lh=e=>{const t=(n=e.value)=>{const r=window==null?void 0:window.document.querySelector("html");r==null||r.classList.toggle("dark",n)};Ye(()=>{lt(e,t,{immediate:!0})}),$a(()=>t())},Qc=(...e)=>{const n=xn().resolve(...e),r=n.matched[n.matched.length-1];if(!(r!=null&&r.redirect))return n;const{redirect:a}=r,o=ie(a)?a(n):a,i=ge(o)?{path:o}:o;return Qc({hash:n.hash,query:n.query,params:n.params,...i})},gi=e=>{const t=Qc(encodeURI(e));return{text:t.meta.title||e,link:t.name==="404"?e:t.fullPath}};let io=null,er=null;const Ph={wait:()=>io,pending:()=>{io=new Promise(e=>er=e)},resolve:()=>{er==null||er(),io=null,er=null}},Zc=()=>Ph,eu=Symbol("sidebarItems"),bi=()=>{const e=Ie(eu);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},Oh=()=>{const e=Ve(),t=It(),n=D(()=>Ih(t.value,e.value));gn(eu,n)},Ih=(e,t)=>{const n=e.sidebar??t.sidebar??"auto",r=e.sidebarDepth??t.sidebarDepth??2;return e.home||n===!1?[]:n==="auto"?Nh(r):te(n)?tu(n,r):si(n)?Mh(n,r):[]},Rh=(e,t)=>({text:e.title,link:e.link,children:yi(e.children,t)}),yi=(e,t)=>t>0?e.map(n=>Rh(n,t-1)):[],Nh=e=>{const t=Jt();return[{text:t.value.title,children:yi(t.value.headers,e)}]},tu=(e,t)=>{const n=En(),r=Jt(),a=o=>{var s;let i;if(ge(o)?i=gi(o):i=o,i.children)return{...i,children:i.children.map(l=>a(l))};if(i.link===n.path){const l=((s=r.value.headers[0])==null?void 0:s.level)===1?r.value.headers[0].children:r.value.headers;return{...i,children:yi(l,t)}}return i};return e.map(o=>a(o))},Mh=(e,t)=>{const n=En(),r=Cc(e,n.path),a=e[r]??[];return tu(a,t)},$h="719px",zh={mobile:$h};var br;(function(e){e.MOBILE="mobile"})(br||(br={}));var xl;const Dh={[br.MOBILE]:Number.parseInt((xl=zh.mobile)==null?void 0:xl.replace("px",""),10)},nu=(e,t)=>{const n=Dh[e];Number.isInteger(n)&&Ye(()=>{t(n),window.addEventListener("resize",()=>t(n),!1),window.addEventListener("orientationchange",()=>t(n),!1)})},Hh={},Fh={class:"theme-default-content"};function jh(e,t){const n=xt("Content");return q(),ne("div",Fh,[re(n)])}const Bh=Ae(Hh,[["render",jh],["__file","HomeContent.vue"]]),Uh={key:0,class:"features"},qh=["innerHTML"],Wh=fe({__name:"HomeFeatures",setup(e){const t=It(),n=D(()=>te(t.value.features)?t.value.features:[]);return(r,a)=>n.value.length?(q(),ne("div",Uh,[(q(!0),ne(_e,null,Zt(n.value,o=>(q(),ne("div",{key:o.title,class:"feature"},[le("h2",null,Ke(o.title),1),Le(" changed from original to allow HTML in features "),le("p",{innerHTML:o.details},null,8,qh)]))),128))])):Le("v-if",!0)}}),ru=Ae(Wh,[["__file","HomeFeatures.vue"]]),Kh=Object.freeze(Object.defineProperty({__proto__:null,default:ru},Symbol.toStringTag,{value:"Module"})),Vh=["innerHTML"],Yh=["textContent"],Gh=fe({__name:"HomeFooter",setup(e){const t=It(),n=D(()=>t.value.footer),r=D(()=>t.value.footerHtml);return(a,o)=>n.value?(q(),ne(_e,{key:0},[Le(" eslint-disable-next-line vue/no-v-html "),r.value?(q(),ne("div",{key:0,class:"footer",innerHTML:n.value},null,8,Vh)):(q(),ne("div",{key:1,class:"footer",textContent:Ke(n.value)},null,8,Yh))],64)):Le("v-if",!0)}}),Xh=Ae(Gh,[["__file","HomeFooter.vue"]]),Jh=["href","rel","target","aria-label"],Qh=fe({inheritAttrs:!1,data(){return{hover:!1}}}),Zh=fe({...Qh,__name:"AutoLink",props:{item:{type:Object,required:!0}},setup(e){const t=e,n=En(),r=Ic(),{item:a}=Ia(t),o=D(()=>Ir(a.value.link)),i=D(()=>gm(a.value.link)||bm(a.value.link)),s=D(()=>{if(!i.value){if(a.value.target)return a.value.target;if(o.value)return"_blank"}}),l=D(()=>s.value==="_blank"),c=D(()=>!o.value&&!i.value&&!l.value),u=D(()=>{if(!i.value){if(a.value.rel)return a.value.rel;if(l.value)return"noopener noreferrer"}}),f=D(()=>a.value.ariaLabel||a.value.text),d=D(()=>{const k=Object.keys(r.value.locales);return k.length?!k.some(E=>E===a.value.link):a.value.link!=="/"}),h=D(()=>d.value?n.path.startsWith(a.value.link):!1),w=D(()=>c.value?a.value.activeMatch?new RegExp(a.value.activeMatch).test(n.path):h.value:!1);return(k,E)=>{const _=xt("RouterLink"),x=xt("font-awesome-icon"),v=xt("AutoLinkExternalIcon");return c.value?(q(),Oe(_,Co({key:0,class:{"router-link-active":w.value},to:Q(a).link,"aria-label":f.value},k.$attrs),{default:De(()=>[ke(k.$slots,"before"),Ct(" "+Ke(Q(a).text)+" ",1),ke(k.$slots,"after")]),_:3},16,["class","to","aria-label"])):(q(),ne("a",Co({key:1,class:"external-link",href:Q(a).link,rel:u.value,target:s.value,"aria-label":f.value,style:Q(a).style},k.$attrs),[ke(k.$slots,"before"),le("span",null,[Q(a).icon?(q(),Oe(Q(Ha),{key:0},{default:De(()=>[re(x,{icon:Q(a).icon,size:"xl",onMouseover:E[0]||(E[0]=b=>k.hover=!0),onMouseleave:E[1]||(E[1]=b=>k.hover=!1),class:Qe({"fa-fade":k.hover})},null,8,["icon","class"])]),_:1})):Le("v-if",!0),Ct(" "+Ke(Q(a).text),1)]),l.value?(q(),Oe(v,{key:0})):Le("v-if",!0),ke(k.$slots,"after")],16,Jh))}}}),ct=Ae(Zh,[["__file","AutoLink.vue"]]),ev=Object.freeze(Object.defineProperty({__proto__:null,default:ct},Symbol.toStringTag,{value:"Module"})),tv={class:"hero"},nv={class:"hero-logo"},rv=le("span",null," .java",-1),av={class:"description"},ov={key:0,class:"actions"},iv=fe({__name:"HomeHero",setup(e){const t=It(),n=ci(),r=vi(),a=D(()=>r.value&&t.value.heroImageDark!==void 0?t.value.heroImageDark:t.value.heroImage),o=D(()=>t.value.heroAlt||s.value||"hero"),i=D(()=>t.value.heroHeight||280),s=D(()=>t.value.heroText===null?null:t.value.heroText||n.value.title||"Hello");D(()=>t.value.tagline===null?null:t.value.tagline||n.value.description||"Welcome to your VuePress site");const l=D(()=>te(t.value.actions)?t.value.actions.map(({text:u,link:f,type:d="primary"})=>({text:u,link:f,type:d})):[]),c=()=>{if(!a.value)return null;const u=ue("img",{src:ui(a.value),alt:o.value,height:i.value});return t.value.heroImageDark===void 0?u:ue(Ha,()=>u)};return(u,f)=>(q(),ne("header",tv,[le("div",nv,[re(c,{style:{margin:"0"}}),rv]),Le(" changed from original template file to remove title and include JMeter link "),le("p",av,[Ct(" Simple "),re(ct,{item:{link:"https://jmeter.apache.org/",text:"JMeter"}},null,8,["item"]),Ct(" performance tests Java API ")]),l.value.length?(q(),ne("p",ov,[(q(!0),ne(_e,null,Zt(l.value,d=>(q(),Oe(ct,{key:d.text,class:Qe(["action-button",[d.type]]),item:d},null,8,["class","item"]))),128))])):Le("v-if",!0)]))}}),au=Ae(iv,[["__file","HomeHero.vue"]]),sv=Object.freeze(Object.defineProperty({__proto__:null,default:au},Symbol.toStringTag,{value:"Module"})),lv={class:"home"},cv=fe({__name:"Home",setup(e){return(t,n)=>(q(),ne("main",lv,[re(au),re(ru),re(Bh),re(Xh)]))}}),uv=Ae(cv,[["__file","Home.vue"]]),fv=le("span",{style:{"font-size":"1.6rem"}}," .java",-1),dv=le("hr",{class:"vertical-divider"},null,-1),mv=fe({__name:"NavbarBrand",setup(e){ic(()=>{let c=document.createElement("noscript");c.innerHTML='',document.body.appendChild(c)});const t=Yn(),n=ci(),r=Ve(),a=vi(),o=D(()=>r.value.home||t.value),i=D(()=>n.value.title),s=D(()=>a.value&&r.value.logoDark!==void 0?r.value.logoDark:r.value.logo),l=()=>{if(!s.value)return null;const c=ue("img",{class:"logo",src:ui(s.value),alt:i.value});return r.value.logoDark===void 0?c:ue(Ha,()=>c)};return(c,u)=>{const f=xt("RouterLink");return q(),ne(_e,null,[re(f,{to:o.value},{default:De(()=>[re(l,{style:{"margin-right":"0"}}),fv,Le(" Stripped site title from original theme ")]),_:1},8,["to"]),dv,re(ct,{item:{link:"https://abstracta.github.io/jmeter-dotnet-dsl",text:".net",style:"font-size: 1.6rem; color: #a502ce"}},null,8,["item"])],64)}}}),ou=Ae(mv,[["__file","NavbarBrand.vue"]]),pv=Object.freeze(Object.defineProperty({__proto__:null,default:ou},Symbol.toStringTag,{value:"Module"})),hv=fe({__name:"DropdownTransition",setup(e){const t=r=>{r.style.height=r.scrollHeight+"px"},n=r=>{r.style.height=""};return(r,a)=>(q(),Oe(Or,{name:"dropdown",onEnter:t,onAfterEnter:n,onBeforeLeave:t},{default:De(()=>[ke(r.$slots,"default")]),_:3}))}}),iu=Ae(hv,[["__file","DropdownTransition.vue"]]),vv=["aria-label"],gv={class:"title"},bv=le("span",{class:"arrow down"},null,-1),yv=["aria-label"],_v={class:"title"},wv={class:"navbar-dropdown"},kv={class:"navbar-dropdown-subtitle"},xv={key:1},Ev={class:"navbar-dropdown-subitem-wrapper"},Cv=fe({__name:"NavbarDropdown",props:{item:{type:Object,required:!0}},setup(e){const t=e,{item:n}=Ia(t),r=D(()=>n.value.ariaLabel||n.value.text),a=ve(!1),o=En();lt(()=>o.path,()=>{a.value=!1});const i=l=>{l.detail===0?a.value=!a.value:a.value=!1},s=(l,c)=>c[c.length-1]===l;return(l,c)=>(q(),ne("div",{class:Qe(["navbar-dropdown-wrapper",{open:a.value}])},[le("button",{class:"navbar-dropdown-title",type:"button","aria-label":r.value,onClick:i},[le("span",gv,Ke(Q(n).text),1),bv],8,vv),le("button",{class:"navbar-dropdown-title-mobile",type:"button","aria-label":r.value,onClick:c[0]||(c[0]=u=>a.value=!a.value)},[le("span",_v,Ke(Q(n).text),1),le("span",{class:Qe(["arrow",a.value?"down":"right"])},null,2)],8,yv),re(iu,null,{default:De(()=>[pa(le("ul",wv,[(q(!0),ne(_e,null,Zt(Q(n).children,u=>(q(),ne("li",{key:u.text,class:"navbar-dropdown-item"},[u.children?(q(),ne(_e,{key:0},[le("h4",kv,[u.link?(q(),Oe(ct,{key:0,item:u,onFocusout:f=>s(u,Q(n).children)&&u.children.length===0&&(a.value=!1)},null,8,["item","onFocusout"])):(q(),ne("span",xv,Ke(u.text),1))]),le("ul",Ev,[(q(!0),ne(_e,null,Zt(u.children,f=>(q(),ne("li",{key:f.link,class:"navbar-dropdown-subitem"},[re(ct,{item:f,onFocusout:d=>s(f,u.children)&&s(u,Q(n).children)&&(a.value=!1)},null,8,["item","onFocusout"])]))),128))])],64)):(q(),Oe(ct,{key:1,item:u,onFocusout:f=>s(u,Q(n).children)&&(a.value=!1)},null,8,["item","onFocusout"]))]))),128))],512),[[ya,a.value]])]),_:1})],2))}}),Sv=Ae(Cv,[["__file","NavbarDropdown.vue"]]),Us=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),Av=(e,t)=>{if(t.hash===e)return!0;const n=Us(t.path),r=Us(e);return n===r},su=(e,t)=>e.link&&Av(e.link,t)?!0:e.children?e.children.some(n=>su(n,t)):!1,lu=e=>!Ir(e)||/github\.com/.test(e)?"GitHub":/bitbucket\.org/.test(e)?"Bitbucket":/gitlab\.com/.test(e)?"GitLab":/gitee\.com/.test(e)?"Gitee":null,Tv={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},Lv=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=lu(e);return n!==null?Tv[n]:null},Pv=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:a})=>{if(!r)return null;const o=Lv({docsRepo:e,editLinkPattern:a});return o?o.replace(/:repo/,Ir(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,Ec(`${xc(n)}/${r}`)):null},Ov={key:0,class:"navbar-items"},Iv=fe({__name:"NavbarItems",setup(e){const t=()=>{const u=xn(),f=Yn(),d=Ic(),h=ci(),w=Ah(),k=Ve();return D(()=>{const E=Object.keys(d.value.locales);if(E.length<2)return[];const _=u.currentRoute.value.path,x=u.currentRoute.value.fullPath;return[{text:`${k.value.selectLanguageText}`,ariaLabel:`${k.value.selectLanguageAriaLabel??k.value.selectLanguageText}`,children:E.map(b=>{var R,J;const P=((R=d.value.locales)==null?void 0:R[b])??{},B=((J=w.value.locales)==null?void 0:J[b])??{},I=`${P.lang}`,g=B.selectLanguageName??I;let M;if(I===h.value.lang)M=x;else{const S=_.replace(f.value,b);u.getRoutes().some($=>$.path===S)?M=x.replace(_,S):M=B.home??b}return{text:g,link:M}})}]})},n=()=>{const u=Ve(),f=D(()=>u.value.repo),d=D(()=>f.value?lu(f.value):null),h=D(()=>f.value&&!Ir(f.value)?`https://github.com/${f.value}`:f.value),w=D(()=>h.value?u.value.repoLabel?u.value.repoLabel:d.value===null?"Source":d.value:null);return D(()=>!h.value||!w.value?[]:[{text:w.value,link:h.value}])},r=u=>ge(u)?gi(u):u.children?{...u,children:u.children.map(r)}:u,a=()=>{const u=Ve();return D(()=>(u.value.navbar||[]).map(r))},o=ve(!1),i=a(),s=t(),l=n(),c=D(()=>[...i.value,...s.value,...l.value]);return nu(br.MOBILE,u=>{window.innerWidthc.value.length?(q(),ne("nav",Ov,[(q(!0),ne(_e,null,Zt(c.value,d=>(q(),ne("div",{key:d.text,class:"navbar-item"},[d.children?(q(),Oe(Sv,{key:0,item:d,class:Qe(o.value?"mobile":"")},null,8,["item","class"])):(q(),Oe(ct,{key:1,item:d},null,8,["item"]))]))),128))])):Le("v-if",!0)}}),cu=Ae(Iv,[["__file","NavbarItems.vue"]]),Rv=["title"],Nv={class:"icon",focusable:"false",viewBox:"0 0 32 32"},Mv=Sd('',9),$v=[Mv],zv={class:"icon",focusable:"false",viewBox:"0 0 32 32"},Dv=le("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1),Hv=[Dv],Fv=fe({__name:"ToggleColorModeButton",setup(e){const t=Ve(),n=vi(),r=()=>{n.value=!n.value};return(a,o)=>(q(),ne("button",{class:"toggle-color-mode-button",title:Q(t).toggleColorMode,onClick:r},[pa((q(),ne("svg",Nv,$v,512)),[[ya,!Q(n)]]),pa((q(),ne("svg",zv,Hv,512)),[[ya,Q(n)]])],8,Rv))}}),jv=Ae(Fv,[["__file","ToggleColorModeButton.vue"]]),Bv=["title"],Uv=le("div",{class:"icon","aria-hidden":"true"},[le("span"),le("span"),le("span")],-1),qv=[Uv],Wv=fe({__name:"ToggleSidebarButton",emits:["toggle"],setup(e){const t=Ve();return(n,r)=>(q(),ne("div",{class:"toggle-sidebar-button",title:Q(t).toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:r[0]||(r[0]=a=>n.$emit("toggle"))},qv,8,Bv))}}),Kv=Ae(Wv,[["__file","ToggleSidebarButton.vue"]]),Vv=fe({__name:"Navbar",emits:["toggle-sidebar"],setup(e){const t=Ve(),n=ve(null),r=ve(null),a=ve(0),o=D(()=>a.value?{maxWidth:a.value+"px"}:{});nu(br.MOBILE,s=>{var c;const l=i(n.value,"paddingLeft")+i(n.value,"paddingRight");window.innerWidth{const c=xt("NavbarSearch");return q(),ne("header",{ref_key:"navbar",ref:n,class:"navbar"},[re(Kv,{onToggle:l[0]||(l[0]=u=>s.$emit("toggle-sidebar"))}),le("span",{ref_key:"navbarBrand",ref:r},[re(ou)],512),le("div",{class:"navbar-items-wrapper",style:Cr(o.value)},[ke(s.$slots,"before"),re(cu,{class:"can-hide"}),ke(s.$slots,"after"),Q(t).colorModeSwitch?(q(),Oe(jv,{key:0})):Le("v-if",!0),re(c)],4)],512)}}}),Yv=Ae(Vv,[["__file","Navbar.vue"]]),Gv={class:"page-meta"},Xv={key:0,class:"meta-item edit-link"},Jv={key:1,class:"meta-item last-updated"},Qv={class:"meta-item-label"},Zv={class:"meta-item-info"},eg={key:2,class:"meta-item contributors"},tg={class:"meta-item-label"},ng={class:"meta-item-info"},rg=["title"],ag=fe({__name:"PageMeta",setup(e){const t=()=>{const l=Ve(),c=Jt(),u=It();return D(()=>{if(!(u.value.editLink??l.value.editLink??!0))return null;const{repo:d,docsRepo:h=d,docsBranch:w="main",docsDir:k="",editLinkText:E}=l.value;if(!h)return null;const _=Pv({docsRepo:h,docsBranch:w,docsDir:k,filePathRelative:c.value.filePathRelative,editLinkPattern:u.value.editLinkPattern??l.value.editLinkPattern});return _?{text:E??"Edit this page",link:_}:null})},n=()=>{const l=Ve(),c=Jt(),u=It();return D(()=>{var h,w;return!(u.value.lastUpdated??l.value.lastUpdated??!0)||!((h=c.value.git)!=null&&h.updatedTime)?null:new Date((w=c.value.git)==null?void 0:w.updatedTime).toLocaleString()})},r=()=>{const l=Ve(),c=Jt(),u=It();return D(()=>{var d;return u.value.contributors??l.value.contributors??!0?((d=c.value.git)==null?void 0:d.contributors)??null:null})},a=Ve(),o=t(),i=n(),s=r();return(l,c)=>{const u=xt("ClientOnly");return q(),ne("footer",Gv,[Q(o)?(q(),ne("div",Xv,[re(ct,{class:"meta-item-label",item:Q(o)},null,8,["item"])])):Le("v-if",!0),Q(i)?(q(),ne("div",Jv,[le("span",Qv,Ke(Q(a).lastUpdatedText)+": ",1),re(u,null,{default:De(()=>[le("span",Zv,Ke(Q(i)),1)]),_:1})])):Le("v-if",!0),Q(s)&&Q(s).length?(q(),ne("div",eg,[le("span",tg,Ke(Q(a).contributorsText)+": ",1),le("span",ng,[(q(!0),ne(_e,null,Zt(Q(s),(f,d)=>(q(),ne(_e,{key:d},[le("span",{class:"contributor",title:`email: ${f.email}`},Ke(f.name),9,rg),d!==Q(s).length-1?(q(),ne(_e,{key:0},[Ct(", ")],64)):Le("v-if",!0)],64))),128))])])):Le("v-if",!0)])}}}),og=Ae(ag,[["__file","PageMeta.vue"]]),ig={key:0,class:"page-nav"},sg={class:"inner"},lg={key:0,class:"prev"},cg={key:1,class:"next"},ug=fe({__name:"PageNav",setup(e){const t=l=>l===!1?null:ge(l)?gi(l):si(l)?l:!1,n=(l,c,u)=>{const f=l.findIndex(d=>d.link===c);if(f!==-1){const d=l[f+u];return d!=null&&d.link?d:null}for(const d of l)if(d.children){const h=n(d.children,c,u);if(h)return h}return null},r=It(),a=bi(),o=En(),i=D(()=>{const l=t(r.value.prev);return l!==!1?l:n(a.value,o.path,-1)}),s=D(()=>{const l=t(r.value.next);return l!==!1?l:n(a.value,o.path,1)});return(l,c)=>i.value||s.value?(q(),ne("nav",ig,[le("p",sg,[i.value?(q(),ne("span",lg,[re(ct,{item:i.value},null,8,["item"])])):Le("v-if",!0),s.value?(q(),ne("span",cg,[re(ct,{item:s.value},null,8,["item"])])):Le("v-if",!0)])])):Le("v-if",!0)}}),fg=Ae(ug,[["__file","PageNav.vue"]]),dg={class:"page"},mg={class:"theme-default-content"},pg=fe({__name:"Page",setup(e){return(t,n)=>{const r=xt("Content");return q(),ne("main",dg,[ke(t.$slots,"top"),le("div",mg,[ke(t.$slots,"content-top"),re(r),ke(t.$slots,"content-bottom")]),re(og),re(fg),ke(t.$slots,"bottom")])}}}),hg=Ae(pg,[["__file","Page.vue"]]),vg=["onKeydown"],gg={class:"sidebar-item-children"},bg=fe({__name:"SidebarItem",props:{item:{type:Object,required:!0},depth:{type:Number,required:!1,default:0}},setup(e){const t=e,{item:n,depth:r}=Ia(t),a=En(),o=xn(),i=D(()=>su(n.value,a)),s=D(()=>({"sidebar-item":!0,"sidebar-heading":r.value===0,active:i.value,collapsible:n.value.collapsible})),l=D(()=>n.value.collapsible?i.value:!0),[c,u]=ph(l.value),f=h=>{n.value.collapsible&&(h.preventDefault(),u())},d=o.afterEach(h=>{Tr(()=>{c.value=l.value})});return Pr(()=>{d()}),(h,w)=>{var E;const k=xt("SidebarItem",!0);return q(),ne("li",null,[Q(n).link?(q(),Oe(ct,{key:0,class:Qe(s.value),item:Q(n)},null,8,["class","item"])):(q(),ne("p",{key:1,tabindex:"0",class:Qe(s.value),onClick:f,onKeydown:lm(f,["enter"])},[Ct(Ke(Q(n).text)+" ",1),Q(n).collapsible?(q(),ne("span",{key:0,class:Qe(["arrow",Q(c)?"down":"right"])},null,2)):Le("v-if",!0)],42,vg)),(E=Q(n).children)!=null&&E.length?(q(),Oe(iu,{key:2},{default:De(()=>[pa(le("ul",gg,[(q(!0),ne(_e,null,Zt(Q(n).children,_=>(q(),Oe(k,{key:`${Q(r)}${_.text}${_.link}`,item:_,depth:Q(r)+1},null,8,["item","depth"]))),128))],512),[[ya,Q(c)]])]),_:1})):Le("v-if",!0)])}}}),yg=Ae(bg,[["__file","SidebarItem.vue"]]),_g={key:0,class:"sidebar-items"},wg=fe({__name:"SidebarItems",setup(e){const t=En(),n=bi();return Ye(()=>{lt(()=>t.hash,r=>{const a=document.querySelector(".sidebar");if(!a)return;const o=document.querySelector(`.sidebar a.sidebar-item[href="${t.path}${r}"]`);if(!o)return;const{top:i,height:s}=a.getBoundingClientRect(),{top:l,height:c}=o.getBoundingClientRect();li+s&&o.scrollIntoView(!1)})}),(r,a)=>Q(n).length?(q(),ne("ul",_g,[(q(!0),ne(_e,null,Zt(Q(n),o=>(q(),Oe(yg,{key:`${o.text}${o.link}`,item:o},null,8,["item"]))),128))])):Le("v-if",!0)}}),kg=Ae(wg,[["__file","SidebarItems.vue"]]),xg={class:"sidebar"},Eg=fe({__name:"Sidebar",setup(e){return(t,n)=>(q(),ne("aside",xg,[re(cu),ke(t.$slots,"top"),re(kg),ke(t.$slots,"bottom")]))}}),Cg=Ae(Eg,[["__file","Sidebar.vue"]]),Sg=fe({__name:"Layout",setup(e){const t=Jt(),n=It(),r=Ve(),a=D(()=>n.value.navbar!==!1&&r.value.navbar!==!1),o=bi(),i=ve(!1),s=E=>{i.value=typeof E=="boolean"?E:!i.value},l={x:0,y:0},c=E=>{l.x=E.changedTouches[0].clientX,l.y=E.changedTouches[0].clientY},u=E=>{const _=E.changedTouches[0].clientX-l.x,x=E.changedTouches[0].clientY-l.y;Math.abs(_)>Math.abs(x)&&Math.abs(_)>40&&(_>0&&l.x<=80?s(!0):s(!1))},f=D(()=>[{"no-navbar":!a.value,"no-sidebar":!o.value.length,"sidebar-open":i.value},n.value.pageClass]);let d;Ye(()=>{d=xn().afterEach(()=>{s(!1)})}),$a(()=>{d()});const h=Zc(),w=h.resolve,k=h.pending;return(E,_)=>(q(),ne("div",{class:Qe(["theme-container",f.value]),onTouchstart:c,onTouchend:u},[ke(E.$slots,"navbar",{},()=>[a.value?(q(),Oe(Yv,{key:0,onToggleSidebar:s},{before:De(()=>[ke(E.$slots,"navbar-before")]),after:De(()=>[ke(E.$slots,"navbar-after")]),_:3})):Le("v-if",!0)]),le("div",{class:"sidebar-mask",onClick:_[0]||(_[0]=x=>s(!1))}),ke(E.$slots,"sidebar",{},()=>[re(Cg,null,{top:De(()=>[ke(E.$slots,"sidebar-top")]),bottom:De(()=>[ke(E.$slots,"sidebar-bottom")]),_:3})]),ke(E.$slots,"page",{},()=>[Q(n).home?(q(),Oe(uv,{key:0})):(q(),Oe(Or,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:Q(w),onBeforeLeave:Q(k)},{default:De(()=>[(q(),Oe(hg,{key:Q(t).path},{top:De(()=>[ke(E.$slots,"page-top")]),"content-top":De(()=>[ke(E.$slots,"page-content-top")]),"content-bottom":De(()=>[ke(E.$slots,"page-content-bottom")]),bottom:De(()=>[ke(E.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34))}}),Ag=Ae(Sg,[["__file","Layout.vue"]]),Tg={class:"theme-container"},Lg={class:"page"},Pg={class:"theme-default-content"},Og=le("h1",null,"404",-1),Ig=fe({__name:"NotFound",setup(e){const t=Yn(),n=Ve(),r=n.value.notFound??["Not Found"],a=()=>r[Math.floor(Math.random()*r.length)],o=n.value.home??t.value,i=n.value.backToHome??"Back to home";return(s,l)=>{const c=xt("RouterLink");return q(),ne("div",Tg,[le("main",Lg,[le("div",Pg,[Og,le("blockquote",null,Ke(a()),1),re(c,{to:Q(o)},{default:De(()=>[Ct(Ke(Q(i)),1)]),_:1},8,["to"])])])])}}}),Rg=Ae(Ig,[["__file","NotFound.vue"]]);const Ng=gt({enhance({app:e,router:t}){e.component("Badge",eh),e.component("CodeGroup",th),e.component("CodeGroupItem",oh),e.component("AutoLinkExternalIcon",()=>{const r=e.component("ExternalLinkIcon");return r?ue(r):null}),e.component("NavbarSearch",()=>{const r=e.component("Docsearch")||e.component("SearchBox");return r?ue(r):null});const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await Zc().wait(),n(...r))},setup(){Th(),Oh()},layouts:{Layout:Ag,NotFound:Rg}}),Mg=e=>e instanceof Element?document.activeElement===e&&(["TEXTAREA","SELECT","INPUT"].includes(e.tagName)||e.hasAttribute("contenteditable")):!1,$g=(e,t)=>t.some(n=>{if(ge(n))return n===e.key;const{key:r,ctrl:a=!1,shift:o=!1,alt:i=!1}=n;return r===e.key&&a===e.ctrlKey&&o===e.shiftKey&&i===e.altKey}),zg=/[^\x00-\x7F]/,Dg=e=>e.split(/\s+/g).map(t=>t.trim()).filter(t=>!!t),qs=e=>e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),Ws=(e,t)=>{const n=t.join(" "),r=Dg(e);if(zg.test(e))return r.some(i=>n.toLowerCase().indexOf(i)>-1);const a=e.endsWith(" ");return new RegExp(r.map((i,s)=>r.length===s+1&&!a?`(?=.*\\b${qs(i)})`:`(?=.*\\b${qs(i)}\\b)`).join("")+".+","gi").test(n)},Hg=({input:e,hotKeys:t})=>{if(t.value.length===0)return;const n=r=>{e.value&&$g(r,t.value)&&!Mg(r.target)&&(r.preventDefault(),e.value.focus())};Ye(()=>{document.addEventListener("keydown",n)}),Pr(()=>{document.removeEventListener("keydown",n)})},Fg=[{title:"",headers:[{level:2,title:"Example",slug:"example",link:"#example",children:[]},{level:2,title:"Hear It From Our Community",slug:"hear-it-from-our-community",link:"#hear-it-from-our-community",children:[]}],path:"/",pathLocale:"/",extraFields:[]},{title:"",headers:[{level:2,title:"Hear It From Our Community",slug:"hear-it-from-our-community",link:"#hear-it-from-our-community",children:[]}],path:"/testimonials.html",pathLocale:"/",extraFields:[]},{title:"User guide",headers:[{level:2,title:"Setup",slug:"setup",link:"#setup",children:[]},{level:2,title:"Simple HTTP test plan",slug:"simple-http-test-plan",link:"#simple-http-test-plan",children:[]},{level:2,title:"DSL recorder",slug:"dsl-recorder",link:"#dsl-recorder",children:[{level:3,title:"Correlations",slug:"correlations",link:"#correlations",children:[]}]},{level:2,title:"DSL code generation from JMX file",slug:"dsl-code-generation-from-jmx-file",link:"#dsl-code-generation-from-jmx-file",children:[]},{level:2,title:"Run test at scale",slug:"run-test-at-scale",link:"#run-test-at-scale",children:[{level:3,title:"BlazeMeter",slug:"blazemeter",link:"#blazemeter",children:[]},{level:3,title:"OctoPerf",slug:"octoperf",link:"#octoperf",children:[]},{level:3,title:"Azure Load Testing",slug:"azure-load-testing",link:"#azure-load-testing",children:[]},{level:3,title:"JMeter remote testing",slug:"jmeter-remote-testing",link:"#jmeter-remote-testing",children:[]}]},{level:2,title:"Auto Stop",slug:"auto-stop",link:"#auto-stop",children:[]},{level:2,title:"Advanced threads configuration",slug:"advanced-threads-configuration",link:"#advanced-threads-configuration",children:[{level:3,title:"Thread ramps and holds",slug:"thread-ramps-and-holds",link:"#thread-ramps-and-holds",children:[]},{level:3,title:"Throughput based thread group",slug:"throughput-based-thread-group",link:"#throughput-based-thread-group",children:[]},{level:3,title:"Set up & tear down",slug:"set-up-tear-down",link:"#set-up-tear-down",children:[]},{level:3,title:"Thread groups order",slug:"thread-groups-order",link:"#thread-groups-order",children:[]}]},{level:2,title:"Test plan debugging",slug:"test-plan-debugging",link:"#test-plan-debugging",children:[{level:3,title:"View results tree",slug:"view-results-tree",link:"#view-results-tree",children:[]},{level:3,title:"Post-processor breakpoints",slug:"post-processor-breakpoints",link:"#post-processor-breakpoints",children:[]},{level:3,title:"Debug info during test plan execution",slug:"debug-info-during-test-plan-execution",link:"#debug-info-during-test-plan-execution",children:[]},{level:3,title:"Debug JMeter code",slug:"debug-jmeter-code",link:"#debug-jmeter-code",children:[]},{level:3,title:"Debug Groovy code",slug:"debug-groovy-code",link:"#debug-groovy-code",children:[]},{level:3,title:"Dummy sampler",slug:"dummy-sampler",link:"#dummy-sampler",children:[]},{level:3,title:"Test plan review in JMeter GUI",slug:"test-plan-review-in-jmeter-gui",link:"#test-plan-review-in-jmeter-gui",children:[]}]},{level:2,title:"Reporting",slug:"reporting",link:"#reporting",children:[{level:3,title:"Log requests and responses",slug:"log-requests-and-responses",link:"#log-requests-and-responses",children:[]},{level:3,title:"Real-time metrics visualization and historic data storage",slug:"real-time-metrics-visualization-and-historic-data-storage",link:"#real-time-metrics-visualization-and-historic-data-storage",children:[{level:4,title:"InfluxDB",slug:"influxdb",link:"#influxdb",children:[]},{level:4,title:"Graphite",slug:"graphite",link:"#graphite",children:[]},{level:4,title:"Elasticsearch",slug:"elasticsearch",link:"#elasticsearch",children:[]},{level:4,title:"Prometheus",slug:"prometheus",link:"#prometheus",children:[]},{level:4,title:"DataDog",slug:"datadog",link:"#datadog",children:[]}]},{level:3,title:"Generate HTML reports from test plan execution",slug:"generate-html-reports-from-test-plan-execution",link:"#generate-html-reports-from-test-plan-execution",children:[]},{level:3,title:"Live built-in graphs and stats",slug:"live-built-in-graphs-and-stats",link:"#live-built-in-graphs-and-stats",children:[]}]},{level:2,title:"Response processing",slug:"response-processing",link:"#response-processing",children:[{level:3,title:"Check for expected response",slug:"check-for-expected-response",link:"#check-for-expected-response",children:[]},{level:3,title:"Check for expected JSON",slug:"check-for-expected-json",link:"#check-for-expected-json",children:[]},{level:3,title:"Change sample result statuses with custom logic",slug:"change-sample-result-statuses-with-custom-logic",link:"#change-sample-result-statuses-with-custom-logic",children:[{level:4,title:"Lambdas",slug:"lambdas",link:"#lambdas",children:[]}]},{level:3,title:"Use part of a response in a subsequent request (aka correlation)",slug:"use-part-of-a-response-in-a-subsequent-request-aka-correlation",link:"#use-part-of-a-response-in-a-subsequent-request-aka-correlation",children:[{level:4,title:"Regular expressions extraction",slug:"regular-expressions-extraction",link:"#regular-expressions-extraction",children:[]},{level:4,title:"Boundaries based extraction",slug:"boundaries-based-extraction",link:"#boundaries-based-extraction",children:[]},{level:4,title:"JSON extraction",slug:"json-extraction",link:"#json-extraction",children:[]}]}]},{level:2,title:"Requests generation",slug:"requests-generation",link:"#requests-generation",children:[{level:3,title:"Conditionals",slug:"conditionals",link:"#conditionals",children:[]},{level:3,title:"Loops",slug:"loops",link:"#loops",children:[{level:4,title:"Iterating over extracted values",slug:"iterating-over-extracted-values",link:"#iterating-over-extracted-values",children:[]},{level:4,title:"Iterating while a condition is met",slug:"iterating-while-a-condition-is-met",link:"#iterating-while-a-condition-is-met",children:[]},{level:4,title:"Iterating a fixed number of times",slug:"iterating-a-fixed-number-of-times",link:"#iterating-a-fixed-number-of-times",children:[]},{level:4,title:"Iterating for a given period",slug:"iterating-for-a-given-period",link:"#iterating-for-a-given-period",children:[]},{level:4,title:"Execute only once in thread",slug:"execute-only-once-in-thread",link:"#execute-only-once-in-thread",children:[]}]},{level:3,title:"Group requests",slug:"group-requests",link:"#group-requests",children:[]},{level:3,title:"CSV as input data for requests",slug:"csv-as-input-data-for-requests",link:"#csv-as-input-data-for-requests",children:[]},{level:3,title:"Counter",slug:"counter",link:"#counter",children:[]},{level:3,title:"Provide request parameters programmatically per request",slug:"provide-request-parameters-programmatically-per-request",link:"#provide-request-parameters-programmatically-per-request",children:[]},{level:3,title:"Timers",slug:"timers",link:"#timers",children:[{level:4,title:"Emulate user delays between requests",slug:"emulate-user-delays-between-requests",link:"#emulate-user-delays-between-requests",children:[]},{level:4,title:"Control throughput",slug:"control-throughput",link:"#control-throughput",children:[]},{level:4,title:"Requests synchronization",slug:"requests-synchronization",link:"#requests-synchronization",children:[]}]},{level:3,title:"Execute part of a test plan part a fraction of the times",slug:"execute-part-of-a-test-plan-part-a-fraction-of-the-times",link:"#execute-part-of-a-test-plan-part-a-fraction-of-the-times",children:[]},{level:3,title:"Switch between test plan parts with a given probability",slug:"switch-between-test-plan-parts-with-a-given-probability",link:"#switch-between-test-plan-parts-with-a-given-probability",children:[]},{level:3,title:"Parallel requests",slug:"parallel-requests",link:"#parallel-requests",children:[]}]},{level:2,title:"JMeter variables & properties",slug:"jmeter-variables-properties",link:"#jmeter-variables-properties",children:[{level:3,title:"Variables",slug:"variables",link:"#variables",children:[]},{level:3,title:"Properties",slug:"properties",link:"#properties",children:[]}]},{level:2,title:"Test resources",slug:"test-resources",link:"#test-resources",children:[]},{level:2,title:"Protocols",slug:"protocols",link:"#protocols",children:[{level:3,title:"HTTP",slug:"http",link:"#http",children:[{level:4,title:"Methods & body",slug:"methods-body",link:"#methods-body",children:[]},{level:4,title:"Parameters",slug:"parameters",link:"#parameters",children:[]},{level:4,title:"Headers",slug:"headers",link:"#headers",children:[]},{level:4,title:"Authentication",slug:"authentication",link:"#authentication",children:[]},{level:4,title:"Multipart requests",slug:"multipart-requests",link:"#multipart-requests",children:[]},{level:4,title:"Cookies & caching",slug:"cookies-caching",link:"#cookies-caching",children:[]},{level:4,title:"Timeouts",slug:"timeouts",link:"#timeouts",children:[]},{level:4,title:"Connections",slug:"connections",link:"#connections",children:[]},{level:4,title:"Embedded resources",slug:"embedded-resources",link:"#embedded-resources",children:[]},{level:4,title:"Redirects",slug:"redirects",link:"#redirects",children:[]},{level:4,title:"HTTP defaults",slug:"http-defaults",link:"#http-defaults",children:[]},{level:4,title:"Overriding URL protocol, host or port",slug:"overriding-url-protocol-host-or-port",link:"#overriding-url-protocol-host-or-port",children:[]},{level:4,title:"Proxy",slug:"proxy",link:"#proxy",children:[]}]},{level:3,title:"GraphQL",slug:"graphql",link:"#graphql",children:[]},{level:3,title:"JDBC and databases interactions",slug:"jdbc-and-databases-interactions",link:"#jdbc-and-databases-interactions",children:[]},{level:3,title:"Java API performance testing",slug:"java-api-performance-testing",link:"#java-api-performance-testing",children:[]},{level:3,title:"Selenium",slug:"selenium",link:"#selenium",children:[]}]},{level:2,title:"Custom or yet not supported test elements",slug:"custom-or-yet-not-supported-test-elements",link:"#custom-or-yet-not-supported-test-elements",children:[]},{level:2,title:"JMX support",slug:"jmx-support",link:"#jmx-support",children:[{level:3,title:"Save as JMX",slug:"save-as-jmx",link:"#save-as-jmx",children:[]},{level:3,title:"Run JMX file",slug:"run-jmx-file",link:"#run-jmx-file",children:[]}]}],path:"/guide/",pathLocale:"/",extraFields:[]},{title:"Motivation",headers:[{level:2,title:"Alternatives analysis",slug:"alternatives-analysis",link:"#alternatives-analysis",children:[{level:3,title:"JMeter",slug:"jmeter",link:"#jmeter",children:[]},{level:3,title:"Gatling",slug:"gatling",link:"#gatling",children:[]},{level:3,title:"Taurus",slug:"taurus",link:"#taurus",children:[]},{level:3,title:"ruby-dsl",slug:"ruby-dsl",link:"#ruby-dsl",children:[]},{level:3,title:"jmeter-java-dsl",slug:"jmeter-java-dsl",link:"#jmeter-java-dsl",children:[]}]},{level:2,title:"Comparison Table",slug:"comparison-table",link:"#comparison-table",children:[]}],path:"/motivation/",pathLocale:"/",extraFields:[]},{title:"Support",headers:[{level:2,title:"Community Support",slug:"community-support",link:"#community-support",children:[]},{level:2,title:"Enterprise Support by Abstracta",slug:"enterprise-support-by-abstracta",link:"#enterprise-support-by-abstracta",children:[]},{level:2,title:"Industry Support",slug:"industry-support",link:"#industry-support",children:[]}],path:"/support/",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/404.html",pathLocale:"/",extraFields:[]}],jg=ve(Fg),Bg=()=>jg,Ug=({searchIndex:e,routeLocale:t,query:n,maxSuggestions:r})=>{const a=D(()=>e.value.filter(o=>o.pathLocale===t.value));return D(()=>{const o=n.value.trim().toLowerCase();if(!o)return[];const i=[],s=(l,c)=>{Ws(o,[c.title])&&i.push({link:`${l.path}#${c.slug}`,title:l.title,header:c.title});for(const u of c.children){if(i.length>=r.value)return;s(l,u)}};for(const l of a.value){if(i.length>=r.value)break;if(Ws(o,[l.title,...l.extraFields])){i.push({link:l.path,title:l.title});continue}for(const c of l.headers){if(i.length>=r.value)break;s(l,c)}}return i})},qg=e=>{const t=ve(0);return{focusIndex:t,focusNext:()=>{t.value{t.value>0?t.value-=1:t.value=e.value.length-1}}},Wg=fe({name:"SearchBox",props:{locales:{type:Object,required:!1,default:()=>({})},hotKeys:{type:Array,required:!1,default:()=>[]},maxSuggestions:{type:Number,required:!1,default:5}},setup(e){const{locales:t,hotKeys:n,maxSuggestions:r}=Ia(e),a=xn(),o=Yn(),i=Bg(),s=ve(null),l=ve(!1),c=ve(""),u=D(()=>t.value[o.value]??{}),f=Ug({searchIndex:i,routeLocale:o,query:c,maxSuggestions:r}),{focusIndex:d,focusNext:h,focusPrev:w}=qg(f);Hg({input:s,hotKeys:n});const k=D(()=>l.value&&!!f.value.length),E=()=>{k.value&&w()},_=()=>{k.value&&h()},x=v=>{if(!k.value)return;const b=f.value[v];b&&a.push(b.link).then(()=>{c.value="",d.value=0})};return()=>ue("form",{class:"search-box",role:"search"},[ue("input",{ref:s,type:"search",placeholder:u.value.placeholder,autocomplete:"off",spellcheck:!1,value:c.value,onFocus:()=>l.value=!0,onBlur:()=>l.value=!1,onInput:v=>c.value=v.target.value,onKeydown:v=>{switch(v.key){case"ArrowUp":{E();break}case"ArrowDown":{_();break}case"Enter":{v.preventDefault(),x(d.value);break}}}}),k.value&&ue("ul",{class:"suggestions",onMouseleave:()=>d.value=-1},f.value.map(({link:v,title:b,header:P},B)=>ue("li",{class:["suggestion",{focus:d.value===B}],onMouseenter:()=>d.value=B,onMousedown:()=>x(B)},ue("a",{href:v,onClick:I=>I.preventDefault()},[ue("span",{class:"page-title"},b),P&&ue("span",{class:"page-header"},`> ${P}`)]))))])}});const Kg={},Vg=["s","/"],Yg=10,Gg=gt({enhance({app:e}){e.component("SearchBox",t=>ue(Wg,{locales:Kg,hotKeys:Vg,maxSuggestions:Yg,...t}))}});const Xg={},Jg=gt({enhance:({app:e})=>{},setup:()=>{}});function Qg(){const e=ve(!1);return ai()&&Ye(()=>{e.value=!0}),e}function Zg(e){return Qg(),D(()=>!!e())}const e1=()=>Zg(()=>typeof window<"u"&&window.navigator&&"userAgent"in window.navigator),t1=()=>{const e=e1();return D(()=>e.value&&/\b(?:Android|iPhone)/i.test(navigator.userAgent))},n1=e=>{const t=Yn();return D(()=>e[t.value])},r1=800,a1=2e3,o1={"/":{copy:"Copy code",copied:"Copied",hint:"Copied successfully"}},i1=!1,s1=['.theme-default-content div[class*="language-"] pre'],Ks=!1,so=new Map,l1=()=>{const{copy:e}=yh({legacy:!0}),t=n1(o1),n=Jt(),r=t1(),a=s=>{if(!s.hasAttribute("copy-code-registered")){const l=document.createElement("button");l.type="button",l.classList.add("copy-code-button"),l.innerHTML='
',l.setAttribute("aria-label",t.value.copy),l.setAttribute("data-copied",t.value.copied),s.parentElement&&s.parentElement.insertBefore(l,s),s.setAttribute("copy-code-registered","")}},o=()=>Tr().then(()=>new Promise(s=>{setTimeout(()=>{s1.forEach(l=>{document.querySelectorAll(l).forEach(a)}),s()},r1)})),i=(s,l,c)=>{let{innerText:u=""}=l;/language-(shellscript|shell|bash|sh|zsh)/.test(s.classList.toString())&&(u=u.replace(/^ *(\$|>) /gm,"")),e(u).then(()=>{c.classList.add("copied"),clearTimeout(so.get(c));const f=setTimeout(()=>{c.classList.remove("copied"),c.blur(),so.delete(c)},a1);so.set(c,f)})};Ye(()=>{(!r.value||Ks)&&o(),ka("click",s=>{const l=s.target;if(l.matches('div[class*="language-"] > button.copy')){const c=l.parentElement,u=l.nextElementSibling;u&&i(c,u,l)}else if(l.matches('div[class*="language-"] div.copy-icon')){const c=l.parentElement,u=c.parentElement,f=c.nextElementSibling;f&&i(u,f,c)}}),lt(()=>n.value.path,()=>{(!r.value||Ks)&&o()})})};var c1=gt({setup:()=>{l1()}});const u1={enhance:({app:e})=>{e.component("AutoLink",it(()=>He(()=>Promise.resolve().then(()=>ev),void 0))),e.component("Carousel",it(()=>He(()=>import("./Carousel-bd2695ae.js"),[]))),e.component("HomeFeatures",it(()=>He(()=>Promise.resolve().then(()=>Kh),void 0))),e.component("HomeHero",it(()=>He(()=>Promise.resolve().then(()=>sv),void 0))),e.component("NavbarBrand",it(()=>He(()=>Promise.resolve().then(()=>pv),void 0))),e.component("Testimonial",it(()=>He(()=>import("./Testimonial-b9405fa9.js"),[])))}};/*! medium-zoom 1.0.8 | MIT License | https://github.com/francoischalifour/medium-zoom */var cn=Object.assign||function(e){for(var t=1;t1&&arguments[1]!==void 0?arguments[1]:{},r=window.Promise||function(S){function $(){}S($,$)},a=function(S){var $=S.target;if($===M){w();return}v.indexOf($)!==-1&&k({target:$})},o=function(){if(!(P||!g.original)){var S=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(B-S)>I.scrollOffset&&setTimeout(w,150)}},i=function(S){var $=S.key||S.keyCode;($==="Escape"||$==="Esc"||$===27)&&w()},s=function(){var S=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},$=S;if(S.background&&(M.style.background=S.background),S.container&&S.container instanceof Object&&($.container=cn({},I.container,S.container)),S.template){var ae=ia(S.template)?S.template:document.querySelector(S.template);$.template=ae}return I=cn({},I,$),v.forEach(function(se){se.dispatchEvent(An("medium-zoom:update",{detail:{zoom:R}}))}),R},l=function(){var S=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};return e(cn({},I,S))},c=function(){for(var S=arguments.length,$=Array(S),ae=0;ae0?$.reduce(function(N,G){return[].concat(N,Ys(G))},[]):v;return se.forEach(function(N){N.classList.remove("medium-zoom-image"),N.dispatchEvent(An("medium-zoom:detach",{detail:{zoom:R}}))}),v=v.filter(function(N){return se.indexOf(N)===-1}),R},f=function(S,$){var ae=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return v.forEach(function(se){se.addEventListener("medium-zoom:"+S,$,ae)}),b.push({type:"medium-zoom:"+S,listener:$,options:ae}),R},d=function(S,$){var ae=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return v.forEach(function(se){se.removeEventListener("medium-zoom:"+S,$,ae)}),b=b.filter(function(se){return!(se.type==="medium-zoom:"+S&&se.listener.toString()===$.toString())}),R},h=function(){var S=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},$=S.target,ae=function(){var N={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},G=void 0,W=void 0;if(I.container)if(I.container instanceof Object)N=cn({},N,I.container),G=N.width-N.left-N.right-I.margin*2,W=N.height-N.top-N.bottom-I.margin*2;else{var $e=ia(I.container)?I.container:document.querySelector(I.container),je=$e.getBoundingClientRect(),Ge=je.width,Ue=je.height,Dt=je.left,Ht=je.top;N=cn({},N,{width:Ge,height:Ue,left:Dt,top:Ht})}G=G||N.width-I.margin*2,W=W||N.height-I.margin*2;var bt=g.zoomedHd||g.original,ze=Vs(bt)?G:bt.naturalWidth||G,A=Vs(bt)?W:bt.naturalHeight||W,U=bt.getBoundingClientRect(),H=U.top,X=U.left,de=U.width,m=U.height,p=Math.min(Math.max(de,ze),G)/de,y=Math.min(Math.max(m,A),W)/m,C=Math.min(p,y),T=(-X+(G-de)/2+I.margin+N.left)/C,L=(-H+(W-m)/2+I.margin+N.top)/C,j="scale("+C+") translate3d("+T+"px, "+L+"px, 0)";g.zoomed.style.transform=j,g.zoomedHd&&(g.zoomedHd.style.transform=j)};return new r(function(se){if($&&v.indexOf($)===-1){se(R);return}var N=function Ge(){P=!1,g.zoomed.removeEventListener("transitionend",Ge),g.original.dispatchEvent(An("medium-zoom:opened",{detail:{zoom:R}})),se(R)};if(g.zoomed){se(R);return}if($)g.original=$;else if(v.length>0){var G=v;g.original=G[0]}else{se(R);return}if(g.original.dispatchEvent(An("medium-zoom:open",{detail:{zoom:R}})),B=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,P=!0,g.zoomed=m1(g.original),document.body.appendChild(M),I.template){var W=ia(I.template)?I.template:document.querySelector(I.template);g.template=document.createElement("div"),g.template.appendChild(W.content.cloneNode(!0)),document.body.appendChild(g.template)}if(g.original.parentElement&&g.original.parentElement.tagName==="PICTURE"&&g.original.currentSrc&&(g.zoomed.src=g.original.currentSrc),document.body.appendChild(g.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),g.original.classList.add("medium-zoom-image--hidden"),g.zoomed.classList.add("medium-zoom-image--opened"),g.zoomed.addEventListener("click",w),g.zoomed.addEventListener("transitionend",N),g.original.getAttribute("data-zoom-src")){g.zoomedHd=g.zoomed.cloneNode(),g.zoomedHd.removeAttribute("srcset"),g.zoomedHd.removeAttribute("sizes"),g.zoomedHd.removeAttribute("loading"),g.zoomedHd.src=g.zoomed.getAttribute("data-zoom-src"),g.zoomedHd.onerror=function(){clearInterval($e),console.warn("Unable to reach the zoom image target "+g.zoomedHd.src),g.zoomedHd=null,ae()};var $e=setInterval(function(){g.zoomedHd.complete&&(clearInterval($e),g.zoomedHd.classList.add("medium-zoom-image--opened"),g.zoomedHd.addEventListener("click",w),document.body.appendChild(g.zoomedHd),ae())},10)}else if(g.original.hasAttribute("srcset")){g.zoomedHd=g.zoomed.cloneNode(),g.zoomedHd.removeAttribute("sizes"),g.zoomedHd.removeAttribute("loading");var je=g.zoomedHd.addEventListener("load",function(){g.zoomedHd.removeEventListener("load",je),g.zoomedHd.classList.add("medium-zoom-image--opened"),g.zoomedHd.addEventListener("click",w),document.body.appendChild(g.zoomedHd),ae()})}else ae()})},w=function(){return new r(function(S){if(P||!g.original){S(R);return}var $=function ae(){g.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(g.zoomed),g.zoomedHd&&document.body.removeChild(g.zoomedHd),document.body.removeChild(M),g.zoomed.classList.remove("medium-zoom-image--opened"),g.template&&document.body.removeChild(g.template),P=!1,g.zoomed.removeEventListener("transitionend",ae),g.original.dispatchEvent(An("medium-zoom:closed",{detail:{zoom:R}})),g.original=null,g.zoomed=null,g.zoomedHd=null,g.template=null,S(R)};P=!0,document.body.classList.remove("medium-zoom--opened"),g.zoomed.style.transform="",g.zoomedHd&&(g.zoomedHd.style.transform=""),g.template&&(g.template.style.transition="opacity 150ms",g.template.style.opacity=0),g.original.dispatchEvent(An("medium-zoom:close",{detail:{zoom:R}})),g.zoomed.addEventListener("transitionend",$)})},k=function(){var S=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},$=S.target;return g.original?w():h({target:$})},E=function(){return I},_=function(){return v},x=function(){return g.original},v=[],b=[],P=!1,B=0,I=n,g={original:null,zoomed:null,zoomedHd:null,template:null};Object.prototype.toString.call(t)==="[object Object]"?I=t:(t||typeof t=="string")&&c(t),I=cn({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},I);var M=d1(I.background);document.addEventListener("click",a),document.addEventListener("keyup",i),document.addEventListener("scroll",o),window.addEventListener("resize",w);var R={open:h,close:w,toggle:k,update:s,clone:l,attach:c,detach:u,on:f,off:d,getOptions:E,getImages:_,getZoomedImage:x};return R};function h1(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 v1=".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}";h1(v1);const g1=p1,b1=Symbol("mediumZoom");const y1="*:is(img):not(.card img):not(a img)",_1={},w1=500,k1=gt({enhance({app:e,router:t}){const n=g1(_1);n.refresh=(r=y1)=>{n.detach(),n.attach(r)},e.provide(b1,n),t.afterEach(()=>{setTimeout(()=>n.refresh(),w1)})}});function Gs(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 K(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n-1;a--){var o=n[a],i=(o.tagName||"").toUpperCase();["STYLE","LINK"].indexOf(i)>-1&&(r=o)}return Ce.head.insertBefore(t,r),e}}var V1="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";function xr(){for(var e=12,t="";e-- >0;)t+=V1[Math.random()*62|0];return t}function Gn(e){for(var t=[],n=(e||[]).length>>>0;n--;)t[n]=e[n];return t}function Ci(e){return e.classList?Gn(e.classList):(e.getAttribute("class")||"").split(" ").filter(function(t){return t})}function wu(e){return"".concat(e).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function Y1(e){return Object.keys(e||{}).reduce(function(t,n){return t+"".concat(n,'="').concat(wu(e[n]),'" ')},"").trim()}function Ba(e){return Object.keys(e||{}).reduce(function(t,n){return t+"".concat(n,": ").concat(e[n].trim(),";")},"")}function Si(e){return e.size!==kt.size||e.x!==kt.x||e.y!==kt.y||e.rotate!==kt.rotate||e.flipX||e.flipY}function G1(e){var t=e.transform,n=e.containerWidth,r=e.iconWidth,a={transform:"translate(".concat(n/2," 256)")},o="translate(".concat(t.x*32,", ").concat(t.y*32,") "),i="scale(".concat(t.size/16*(t.flipX?-1:1),", ").concat(t.size/16*(t.flipY?-1:1),") "),s="rotate(".concat(t.rotate," 0 0)"),l={transform:"".concat(o," ").concat(i," ").concat(s)},c={transform:"translate(".concat(r/2*-1," -256)")};return{outer:a,inner:l,path:c}}function X1(e){var t=e.transform,n=e.width,r=n===void 0?Oo:n,a=e.height,o=a===void 0?Oo:a,i=e.startCentered,s=i===void 0?!1:i,l="";return s&&pu?l+="translate(".concat(t.x/qt-r/2,"em, ").concat(t.y/qt-o/2,"em) "):s?l+="translate(calc(-50% + ".concat(t.x/qt,"em), calc(-50% + ").concat(t.y/qt,"em)) "):l+="translate(".concat(t.x/qt,"em, ").concat(t.y/qt,"em) "),l+="scale(".concat(t.size/qt*(t.flipX?-1:1),", ").concat(t.size/qt*(t.flipY?-1:1),") "),l+="rotate(".concat(t.rotate,"deg) "),l}var J1=`:root, :host { + --fa-font-solid: normal 900 1em/1 "Font Awesome 6 Solid"; + --fa-font-regular: normal 400 1em/1 "Font Awesome 6 Regular"; + --fa-font-light: normal 300 1em/1 "Font Awesome 6 Light"; + --fa-font-thin: normal 100 1em/1 "Font Awesome 6 Thin"; + --fa-font-duotone: normal 900 1em/1 "Font Awesome 6 Duotone"; + --fa-font-sharp-solid: normal 900 1em/1 "Font Awesome 6 Sharp"; + --fa-font-sharp-regular: normal 400 1em/1 "Font Awesome 6 Sharp"; + --fa-font-sharp-light: normal 300 1em/1 "Font Awesome 6 Sharp"; + --fa-font-brands: normal 400 1em/1 "Font Awesome 6 Brands"; +} + +svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa { + overflow: visible; + box-sizing: content-box; +} + +.svg-inline--fa { + display: var(--fa-display, inline-block); + height: 1em; + overflow: visible; + vertical-align: -0.125em; +} +.svg-inline--fa.fa-2xs { + vertical-align: 0.1em; +} +.svg-inline--fa.fa-xs { + vertical-align: 0em; +} +.svg-inline--fa.fa-sm { + vertical-align: -0.0714285705em; +} +.svg-inline--fa.fa-lg { + vertical-align: -0.2em; +} +.svg-inline--fa.fa-xl { + vertical-align: -0.25em; +} +.svg-inline--fa.fa-2xl { + vertical-align: -0.3125em; +} +.svg-inline--fa.fa-pull-left { + margin-right: var(--fa-pull-margin, 0.3em); + width: auto; +} +.svg-inline--fa.fa-pull-right { + margin-left: var(--fa-pull-margin, 0.3em); + width: auto; +} +.svg-inline--fa.fa-li { + width: var(--fa-li-width, 2em); + top: 0.25em; +} +.svg-inline--fa.fa-fw { + width: var(--fa-fw-width, 1.25em); +} + +.fa-layers svg.svg-inline--fa { + bottom: 0; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; +} + +.fa-layers-counter, .fa-layers-text { + display: inline-block; + position: absolute; + text-align: center; +} + +.fa-layers { + display: inline-block; + height: 1em; + position: relative; + text-align: center; + vertical-align: -0.125em; + width: 1em; +} +.fa-layers svg.svg-inline--fa { + -webkit-transform-origin: center center; + transform-origin: center center; +} + +.fa-layers-text { + left: 50%; + top: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + -webkit-transform-origin: center center; + transform-origin: center center; +} + +.fa-layers-counter { + background-color: var(--fa-counter-background-color, #ff253a); + border-radius: var(--fa-counter-border-radius, 1em); + box-sizing: border-box; + color: var(--fa-inverse, #fff); + line-height: var(--fa-counter-line-height, 1); + max-width: var(--fa-counter-max-width, 5em); + min-width: var(--fa-counter-min-width, 1.5em); + overflow: hidden; + padding: var(--fa-counter-padding, 0.25em 0.5em); + right: var(--fa-right, 0); + text-overflow: ellipsis; + top: var(--fa-top, 0); + -webkit-transform: scale(var(--fa-counter-scale, 0.25)); + transform: scale(var(--fa-counter-scale, 0.25)); + -webkit-transform-origin: top right; + transform-origin: top right; +} + +.fa-layers-bottom-right { + bottom: var(--fa-bottom, 0); + right: var(--fa-right, 0); + top: auto; + -webkit-transform: scale(var(--fa-layers-scale, 0.25)); + transform: scale(var(--fa-layers-scale, 0.25)); + -webkit-transform-origin: bottom right; + transform-origin: bottom right; +} + +.fa-layers-bottom-left { + bottom: var(--fa-bottom, 0); + left: var(--fa-left, 0); + right: auto; + top: auto; + -webkit-transform: scale(var(--fa-layers-scale, 0.25)); + transform: scale(var(--fa-layers-scale, 0.25)); + -webkit-transform-origin: bottom left; + transform-origin: bottom left; +} + +.fa-layers-top-right { + top: var(--fa-top, 0); + right: var(--fa-right, 0); + -webkit-transform: scale(var(--fa-layers-scale, 0.25)); + transform: scale(var(--fa-layers-scale, 0.25)); + -webkit-transform-origin: top right; + transform-origin: top right; +} + +.fa-layers-top-left { + left: var(--fa-left, 0); + right: auto; + top: var(--fa-top, 0); + -webkit-transform: scale(var(--fa-layers-scale, 0.25)); + transform: scale(var(--fa-layers-scale, 0.25)); + -webkit-transform-origin: top left; + transform-origin: top left; +} + +.fa-1x { + font-size: 1em; +} + +.fa-2x { + font-size: 2em; +} + +.fa-3x { + font-size: 3em; +} + +.fa-4x { + font-size: 4em; +} + +.fa-5x { + font-size: 5em; +} + +.fa-6x { + font-size: 6em; +} + +.fa-7x { + font-size: 7em; +} + +.fa-8x { + font-size: 8em; +} + +.fa-9x { + font-size: 9em; +} + +.fa-10x { + font-size: 10em; +} + +.fa-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; +} + +.fa-xs { + font-size: 0.75em; + line-height: 0.0833333337em; + vertical-align: 0.125em; +} + +.fa-sm { + font-size: 0.875em; + line-height: 0.0714285718em; + vertical-align: 0.0535714295em; +} + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; +} + +.fa-xl { + font-size: 1.5em; + line-height: 0.0416666682em; + vertical-align: -0.125em; +} + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; +} + +.fa-fw { + text-align: center; + width: 1.25em; +} + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; +} +.fa-ul > li { + position: relative; +} + +.fa-li { + left: calc(var(--fa-li-width, 2em) * -1); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; +} + +.fa-border { + border-color: var(--fa-border-color, #eee); + border-radius: var(--fa-border-radius, 0.1em); + border-style: var(--fa-border-style, solid); + border-width: var(--fa-border-width, 0.08em); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); +} + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); +} + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); +} + +.fa-beat { + -webkit-animation-name: fa-beat; + animation-name: fa-beat; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); +} + +.fa-bounce { + -webkit-animation-name: fa-bounce; + animation-name: fa-bounce; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); +} + +.fa-fade { + -webkit-animation-name: fa-fade; + animation-name: fa-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); +} + +.fa-beat-fade { + -webkit-animation-name: fa-beat-fade; + animation-name: fa-beat-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); +} + +.fa-flip { + -webkit-animation-name: fa-flip; + animation-name: fa-flip; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); +} + +.fa-shake { + -webkit-animation-name: fa-shake; + animation-name: fa-shake; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); +} + +.fa-spin { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 2s); + animation-duration: var(--fa-animation-duration, 2s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); +} + +.fa-spin-reverse { + --fa-animation-direction: reverse; +} + +.fa-pulse, +.fa-spin-pulse { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); + animation-timing-function: var(--fa-animation-timing, steps(8)); +} + +@media (prefers-reduced-motion: reduce) { + .fa-beat, +.fa-bounce, +.fa-fade, +.fa-beat-fade, +.fa-flip, +.fa-pulse, +.fa-shake, +.fa-spin, +.fa-spin-pulse { + -webkit-animation-delay: -1ms; + animation-delay: -1ms; + -webkit-animation-duration: 1ms; + animation-duration: 1ms; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; + -webkit-transition-delay: 0s; + transition-delay: 0s; + -webkit-transition-duration: 0s; + transition-duration: 0s; + } +} +@-webkit-keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); + } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); + } +} +@keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); + } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); + } +} +@-webkit-keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } +} +@keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); + } +} +@-webkit-keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); + } +} +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); + } +} +@-webkit-keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); + } +} +@keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); + } +} +@-webkit-keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + } +} +@keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + } +} +@-webkit-keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); + } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); + } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); + } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); + } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); + } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); + } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); + } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); + } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} +@keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); + } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); + } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); + } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); + } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); + } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); + } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); + } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); + } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +.fa-rotate-90 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.fa-rotate-180 { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); +} + +.fa-rotate-270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); +} + +.fa-flip-horizontal { + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); +} + +.fa-flip-vertical { + -webkit-transform: scale(1, -1); + transform: scale(1, -1); +} + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); +} + +.fa-rotate-by { + -webkit-transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, none)); +} + +.fa-stack { + display: inline-block; + vertical-align: middle; + height: 2em; + position: relative; + width: 2.5em; +} + +.fa-stack-1x, +.fa-stack-2x { + bottom: 0; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; + z-index: var(--fa-stack-z-index, auto); +} + +.svg-inline--fa.fa-stack-1x { + height: 1em; + width: 1.25em; +} +.svg-inline--fa.fa-stack-2x { + height: 2em; + width: 2.5em; +} + +.fa-inverse { + color: var(--fa-inverse, #fff); +} + +.sr-only, +.fa-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; +} + +.sr-only-focusable:not(:focus), +.fa-sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.svg-inline--fa .fa-primary { + fill: var(--fa-primary-color, currentColor); + opacity: var(--fa-primary-opacity, 1); +} + +.svg-inline--fa .fa-secondary { + fill: var(--fa-secondary-color, currentColor); + opacity: var(--fa-secondary-opacity, 0.4); +} + +.svg-inline--fa.fa-swap-opacity .fa-primary { + opacity: var(--fa-secondary-opacity, 0.4); +} + +.svg-inline--fa.fa-swap-opacity .fa-secondary { + opacity: var(--fa-primary-opacity, 1); +} + +.svg-inline--fa mask .fa-primary, +.svg-inline--fa mask .fa-secondary { + fill: black; +} + +.fad.fa-inverse, +.fa-duotone.fa-inverse { + color: var(--fa-inverse, #fff); +}`;function ku(){var e=hu,t=vu,n=Y.cssPrefix,r=Y.replacementClass,a=J1;if(n!==e||r!==t){var o=new RegExp("\\.".concat(e,"\\-"),"g"),i=new RegExp("\\--".concat(e,"\\-"),"g"),s=new RegExp("\\.".concat(t),"g");a=a.replace(o,".".concat(n,"-")).replace(i,"--".concat(n,"-")).replace(s,".".concat(r))}return a}var nl=!1;function lo(){Y.autoAddCss&&!nl&&(K1(ku()),nl=!0)}var Q1={mixout:function(){return{dom:{css:ku,insertCss:lo}}},hooks:function(){return{beforeDOMElementCreation:function(){lo()},beforeI2svg:function(){lo()}}}},Nt=nn||{};Nt[Rt]||(Nt[Rt]={});Nt[Rt].styles||(Nt[Rt].styles={});Nt[Rt].hooks||(Nt[Rt].hooks={});Nt[Rt].shims||(Nt[Rt].shims=[]);var pt=Nt[Rt],xu=[],Z1=function e(){Ce.removeEventListener("DOMContentLoaded",e),Ea=1,xu.map(function(t){return t()})},Ea=!1;zt&&(Ea=(Ce.documentElement.doScroll?/^loaded|^c/:/^loaded|^i|^c/).test(Ce.readyState),Ea||Ce.addEventListener("DOMContentLoaded",Z1));function eb(e){zt&&(Ea?setTimeout(e,0):xu.push(e))}function Mr(e){var t=e.tag,n=e.attributes,r=n===void 0?{}:n,a=e.children,o=a===void 0?[]:a;return typeof e=="string"?wu(e):"<".concat(t," ").concat(Y1(r),">").concat(o.map(Mr).join(""),"")}function rl(e,t,n){if(e&&e[t]&&e[t][n])return{prefix:t,iconName:n,icon:e[t][n]}}var tb=function(t,n){return function(r,a,o,i){return t.call(n,r,a,o,i)}},co=function(t,n,r,a){var o=Object.keys(t),i=o.length,s=a!==void 0?tb(n,a):n,l,c,u;for(r===void 0?(l=1,u=t[o[0]]):(l=0,u=r);l=55296&&a<=56319&&n=55296&&r<=56319&&n>t+1&&(a=e.charCodeAt(t+1),a>=56320&&a<=57343)?(r-55296)*1024+a-56320+65536:r}function al(e){return Object.keys(e).reduce(function(t,n){var r=e[n],a=!!r.icon;return a?t[r.iconName]=r.icon:t[n]=r,t},{})}function No(e,t){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},r=n.skipHooks,a=r===void 0?!1:r,o=al(t);typeof pt.hooks.addPack=="function"&&!a?pt.hooks.addPack(e,al(t)):pt.styles[e]=K(K({},pt.styles[e]||{}),o),e==="fas"&&No("fa",t)}var ta,na,ra,Pn=pt.styles,ab=pt.shims,ob=(ta={},Me(ta,Ee,Object.values(wr[Ee])),Me(ta,Te,Object.values(wr[Te])),ta),Ai=null,Eu={},Cu={},Su={},Au={},Tu={},ib=(na={},Me(na,Ee,Object.keys(yr[Ee])),Me(na,Te,Object.keys(yr[Te])),na);function sb(e){return~j1.indexOf(e)}function lb(e,t){var n=t.split("-"),r=n[0],a=n.slice(1).join("-");return r===e&&a!==""&&!sb(a)?a:null}var Lu=function(){var t=function(o){return co(Pn,function(i,s,l){return i[l]=co(s,o,{}),i},{})};Eu=t(function(a,o,i){if(o[3]&&(a[o[3]]=i),o[2]){var s=o[2].filter(function(l){return typeof l=="number"});s.forEach(function(l){a[l.toString(16)]=i})}return a}),Cu=t(function(a,o,i){if(a[i]=i,o[2]){var s=o[2].filter(function(l){return typeof l=="string"});s.forEach(function(l){a[l]=i})}return a}),Tu=t(function(a,o,i){var s=o[2];return a[i]=i,s.forEach(function(l){a[l]=i}),a});var n="far"in Pn||Y.autoFetchSvg,r=co(ab,function(a,o){var i=o[0],s=o[1],l=o[2];return s==="far"&&!n&&(s="fas"),typeof i=="string"&&(a.names[i]={prefix:s,iconName:l}),typeof i=="number"&&(a.unicodes[i.toString(16)]={prefix:s,iconName:l}),a},{names:{},unicodes:{}});Su=r.names,Au=r.unicodes,Ai=Ua(Y.styleDefault,{family:Y.familyDefault})};W1(function(e){Ai=Ua(e.styleDefault,{family:Y.familyDefault})});Lu();function Ti(e,t){return(Eu[e]||{})[t]}function cb(e,t){return(Cu[e]||{})[t]}function hn(e,t){return(Tu[e]||{})[t]}function Pu(e){return Su[e]||{prefix:null,iconName:null}}function ub(e){var t=Au[e],n=Ti("fas",e);return t||(n?{prefix:"fas",iconName:n}:null)||{prefix:null,iconName:null}}function rn(){return Ai}var Li=function(){return{prefix:null,iconName:null,rest:[]}};function Ua(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=t.family,r=n===void 0?Ee:n,a=yr[r][e],o=_r[r][e]||_r[r][a],i=e in pt.styles?e:null;return o||i||null}var ol=(ra={},Me(ra,Ee,Object.keys(wr[Ee])),Me(ra,Te,Object.keys(wr[Te])),ra);function qa(e){var t,n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=n.skipLookups,a=r===void 0?!1:r,o=(t={},Me(t,Ee,"".concat(Y.cssPrefix,"-").concat(Ee)),Me(t,Te,"".concat(Y.cssPrefix,"-").concat(Te)),t),i=null,s=Ee;(e.includes(o[Ee])||e.some(function(c){return ol[Ee].includes(c)}))&&(s=Ee),(e.includes(o[Te])||e.some(function(c){return ol[Te].includes(c)}))&&(s=Te);var l=e.reduce(function(c,u){var f=lb(Y.cssPrefix,u);if(Pn[u]?(u=ob[s].includes(u)?M1[s][u]:u,i=u,c.prefix=u):ib[s].indexOf(u)>-1?(i=u,c.prefix=Ua(u,{family:s})):f?c.iconName=f:u!==Y.replacementClass&&u!==o[Ee]&&u!==o[Te]&&c.rest.push(u),!a&&c.prefix&&c.iconName){var d=i==="fa"?Pu(c.iconName):{},h=hn(c.prefix,c.iconName);d.prefix&&(i=null),c.iconName=d.iconName||h||c.iconName,c.prefix=d.prefix||c.prefix,c.prefix==="far"&&!Pn.far&&Pn.fas&&!Y.autoFetchSvg&&(c.prefix="fas")}return c},Li());return(e.includes("fa-brands")||e.includes("fab"))&&(l.prefix="fab"),(e.includes("fa-duotone")||e.includes("fad"))&&(l.prefix="fad"),!l.prefix&&s===Te&&(Pn.fass||Y.autoFetchSvg)&&(l.prefix="fass",l.iconName=hn(l.prefix,l.iconName)||l.iconName),(l.prefix==="fa"||i==="fa")&&(l.prefix=rn()||"fas"),l}var fb=function(){function e(){x1(this,e),this.definitions={}}return E1(e,[{key:"add",value:function(){for(var n=this,r=arguments.length,a=new Array(r),o=0;o0&&u.forEach(function(f){typeof f=="string"&&(n[s][f]=c)}),n[s][l]=c}),n}}]),e}(),il=[],On={},zn={},db=Object.keys(zn);function mb(e,t){var n=t.mixoutsTo;return il=e,On={},Object.keys(zn).forEach(function(r){db.indexOf(r)===-1&&delete zn[r]}),il.forEach(function(r){var a=r.mixout?r.mixout():{};if(Object.keys(a).forEach(function(i){typeof a[i]=="function"&&(n[i]=a[i]),xa(a[i])==="object"&&Object.keys(a[i]).forEach(function(s){n[i]||(n[i]={}),n[i][s]=a[i][s]})}),r.hooks){var o=r.hooks();Object.keys(o).forEach(function(i){On[i]||(On[i]=[]),On[i].push(o[i])})}r.provides&&r.provides(zn)}),n}function Mo(e,t){for(var n=arguments.length,r=new Array(n>2?n-2:0),a=2;a1?t-1:0),r=1;r0&&arguments[0]!==void 0?arguments[0]:{};return zt?(wn("beforeI2svg",t),Mt("pseudoElements2svg",t),Mt("i2svg",t)):Promise.reject("Operation requires a DOM of some kind.")},watch:function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},n=t.autoReplaceSvgRoot;Y.autoReplaceSvg===!1&&(Y.autoReplaceSvg=!0),Y.observeMutations=!0,eb(function(){gb({autoReplaceSvgRoot:n}),wn("watch",t)})}},vb={icon:function(t){if(t===null)return null;if(xa(t)==="object"&&t.prefix&&t.iconName)return{prefix:t.prefix,iconName:hn(t.prefix,t.iconName)||t.iconName};if(Array.isArray(t)&&t.length===2){var n=t[1].indexOf("fa-")===0?t[1].slice(3):t[1],r=Ua(t[0]);return{prefix:r,iconName:hn(r,n)||n}}if(typeof t=="string"&&(t.indexOf("".concat(Y.cssPrefix,"-"))>-1||t.match($1))){var a=qa(t.split(" "),{skipLookups:!0});return{prefix:a.prefix||rn(),iconName:hn(a.prefix,a.iconName)||a.iconName}}if(typeof t=="string"){var o=rn();return{prefix:o,iconName:hn(o,t)||t}}}},rt={noAuto:pb,config:Y,dom:hb,parse:vb,library:Ou,findIconDefinition:$o,toHtml:Mr},gb=function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},n=t.autoReplaceSvgRoot,r=n===void 0?Ce:n;(Object.keys(pt.styles).length>0||Y.autoFetchSvg)&&zt&&Y.autoReplaceSvg&&rt.dom.i2svg({node:r})};function Wa(e,t){return Object.defineProperty(e,"abstract",{get:t}),Object.defineProperty(e,"html",{get:function(){return e.abstract.map(function(r){return Mr(r)})}}),Object.defineProperty(e,"node",{get:function(){if(zt){var r=Ce.createElement("div");return r.innerHTML=e.html,r.children}}}),e}function bb(e){var t=e.children,n=e.main,r=e.mask,a=e.attributes,o=e.styles,i=e.transform;if(Si(i)&&n.found&&!r.found){var s=n.width,l=n.height,c={x:s/l/2,y:.5};a.style=Ba(K(K({},o),{},{"transform-origin":"".concat(c.x+i.x/16,"em ").concat(c.y+i.y/16,"em")}))}return[{tag:"svg",attributes:a,children:t}]}function yb(e){var t=e.prefix,n=e.iconName,r=e.children,a=e.attributes,o=e.symbol,i=o===!0?"".concat(t,"-").concat(Y.cssPrefix,"-").concat(n):o;return[{tag:"svg",attributes:{style:"display: none;"},children:[{tag:"symbol",attributes:K(K({},a),{},{id:i}),children:r}]}]}function Pi(e){var t=e.icons,n=t.main,r=t.mask,a=e.prefix,o=e.iconName,i=e.transform,s=e.symbol,l=e.title,c=e.maskId,u=e.titleId,f=e.extra,d=e.watchable,h=d===void 0?!1:d,w=r.found?r:n,k=w.width,E=w.height,_=a==="fak",x=[Y.replacementClass,o?"".concat(Y.cssPrefix,"-").concat(o):""].filter(function(M){return f.classes.indexOf(M)===-1}).filter(function(M){return M!==""||!!M}).concat(f.classes).join(" "),v={children:[],attributes:K(K({},f.attributes),{},{"data-prefix":a,"data-icon":o,class:x,role:f.attributes.role||"img",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 ".concat(k," ").concat(E)})},b=_&&!~f.classes.indexOf("fa-fw")?{width:"".concat(k/E*16*.0625,"em")}:{};h&&(v.attributes[_n]=""),l&&(v.children.push({tag:"title",attributes:{id:v.attributes["aria-labelledby"]||"title-".concat(u||xr())},children:[l]}),delete v.attributes.title);var P=K(K({},v),{},{prefix:a,iconName:o,main:n,mask:r,maskId:c,transform:i,symbol:s,styles:K(K({},b),f.styles)}),B=r.found&&n.found?Mt("generateAbstractMask",P)||{children:[],attributes:{}}:Mt("generateAbstractIcon",P)||{children:[],attributes:{}},I=B.children,g=B.attributes;return P.children=I,P.attributes=g,s?yb(P):bb(P)}function sl(e){var t=e.content,n=e.width,r=e.height,a=e.transform,o=e.title,i=e.extra,s=e.watchable,l=s===void 0?!1:s,c=K(K(K({},i.attributes),o?{title:o}:{}),{},{class:i.classes.join(" ")});l&&(c[_n]="");var u=K({},i.styles);Si(a)&&(u.transform=X1({transform:a,startCentered:!0,width:n,height:r}),u["-webkit-transform"]=u.transform);var f=Ba(u);f.length>0&&(c.style=f);var d=[];return d.push({tag:"span",attributes:c,children:[t]}),o&&d.push({tag:"span",attributes:{class:"sr-only"},children:[o]}),d}function _b(e){var t=e.content,n=e.title,r=e.extra,a=K(K(K({},r.attributes),n?{title:n}:{}),{},{class:r.classes.join(" ")}),o=Ba(r.styles);o.length>0&&(a.style=o);var i=[];return i.push({tag:"span",attributes:a,children:[t]}),n&&i.push({tag:"span",attributes:{class:"sr-only"},children:[n]}),i}var uo=pt.styles;function zo(e){var t=e[0],n=e[1],r=e.slice(4),a=_i(r,1),o=a[0],i=null;return Array.isArray(o)?i={tag:"g",attributes:{class:"".concat(Y.cssPrefix,"-").concat(pn.GROUP)},children:[{tag:"path",attributes:{class:"".concat(Y.cssPrefix,"-").concat(pn.SECONDARY),fill:"currentColor",d:o[0]}},{tag:"path",attributes:{class:"".concat(Y.cssPrefix,"-").concat(pn.PRIMARY),fill:"currentColor",d:o[1]}}]}:i={tag:"path",attributes:{fill:"currentColor",d:o}},{found:!0,width:t,height:n,icon:i}}var wb={found:!1,width:512,height:512};function kb(e,t){!gu&&!Y.showMissingIcons&&e&&console.error('Icon with name "'.concat(e,'" and prefix "').concat(t,'" is missing.'))}function Do(e,t){var n=t;return t==="fa"&&Y.styleDefault!==null&&(t=rn()),new Promise(function(r,a){if(Mt("missingIconAbstract"),n==="fa"){var o=Pu(e)||{};e=o.iconName||e,t=o.prefix||t}if(e&&t&&uo[t]&&uo[t][e]){var i=uo[t][e];return r(zo(i))}kb(e,t),r(K(K({},wb),{},{icon:Y.showMissingIcons&&e?Mt("missingIconAbstract")||{}:{}}))})}var ll=function(){},Ho=Y.measurePerformance&&Gr&&Gr.mark&&Gr.measure?Gr:{mark:ll,measure:ll},rr='FA "6.4.2"',xb=function(t){return Ho.mark("".concat(rr," ").concat(t," begins")),function(){return Iu(t)}},Iu=function(t){Ho.mark("".concat(rr," ").concat(t," ends")),Ho.measure("".concat(rr," ").concat(t),"".concat(rr," ").concat(t," begins"),"".concat(rr," ").concat(t," ends"))},Oi={begin:xb,end:Iu},sa=function(){};function cl(e){var t=e.getAttribute?e.getAttribute(_n):null;return typeof t=="string"}function Eb(e){var t=e.getAttribute?e.getAttribute(ki):null,n=e.getAttribute?e.getAttribute(xi):null;return t&&n}function Cb(e){return e&&e.classList&&e.classList.contains&&e.classList.contains(Y.replacementClass)}function Sb(){if(Y.autoReplaceSvg===!0)return la.replace;var e=la[Y.autoReplaceSvg];return e||la.replace}function Ab(e){return Ce.createElementNS("http://www.w3.org/2000/svg",e)}function Tb(e){return Ce.createElement(e)}function Ru(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=t.ceFn,r=n===void 0?e.tag==="svg"?Ab:Tb:n;if(typeof e=="string")return Ce.createTextNode(e);var a=r(e.tag);Object.keys(e.attributes||[]).forEach(function(i){a.setAttribute(i,e.attributes[i])});var o=e.children||[];return o.forEach(function(i){a.appendChild(Ru(i,{ceFn:r}))}),a}function Lb(e){var t=" ".concat(e.outerHTML," ");return t="".concat(t,"Font Awesome fontawesome.com "),t}var la={replace:function(t){var n=t[0];if(n.parentNode)if(t[1].forEach(function(a){n.parentNode.insertBefore(Ru(a),n)}),n.getAttribute(_n)===null&&Y.keepOriginalSource){var r=Ce.createComment(Lb(n));n.parentNode.replaceChild(r,n)}else n.remove()},nest:function(t){var n=t[0],r=t[1];if(~Ci(n).indexOf(Y.replacementClass))return la.replace(t);var a=new RegExp("".concat(Y.cssPrefix,"-.*"));if(delete r[0].attributes.id,r[0].attributes.class){var o=r[0].attributes.class.split(" ").reduce(function(s,l){return l===Y.replacementClass||l.match(a)?s.toSvg.push(l):s.toNode.push(l),s},{toNode:[],toSvg:[]});r[0].attributes.class=o.toSvg.join(" "),o.toNode.length===0?n.removeAttribute("class"):n.setAttribute("class",o.toNode.join(" "))}var i=r.map(function(s){return Mr(s)}).join(` +`);n.setAttribute(_n,""),n.innerHTML=i}};function ul(e){e()}function Nu(e,t){var n=typeof t=="function"?t:sa;if(e.length===0)n();else{var r=ul;Y.mutateApproach===R1&&(r=nn.requestAnimationFrame||ul),r(function(){var a=Sb(),o=Oi.begin("mutate");e.map(a),o(),n()})}}var Ii=!1;function Mu(){Ii=!0}function Fo(){Ii=!1}var Ca=null;function fl(e){if(el&&Y.observeMutations){var t=e.treeCallback,n=t===void 0?sa:t,r=e.nodeCallback,a=r===void 0?sa:r,o=e.pseudoElementsCallback,i=o===void 0?sa:o,s=e.observeMutationsRoot,l=s===void 0?Ce:s;Ca=new el(function(c){if(!Ii){var u=rn();Gn(c).forEach(function(f){if(f.type==="childList"&&f.addedNodes.length>0&&!cl(f.addedNodes[0])&&(Y.searchPseudoElements&&i(f.target),n(f.target)),f.type==="attributes"&&f.target.parentNode&&Y.searchPseudoElements&&i(f.target.parentNode),f.type==="attributes"&&cl(f.target)&&~F1.indexOf(f.attributeName))if(f.attributeName==="class"&&Eb(f.target)){var d=qa(Ci(f.target)),h=d.prefix,w=d.iconName;f.target.setAttribute(ki,h||u),w&&f.target.setAttribute(xi,w)}else Cb(f.target)&&a(f.target)})}}),zt&&Ca.observe(l,{childList:!0,attributes:!0,characterData:!0,subtree:!0})}}function Pb(){Ca&&Ca.disconnect()}function Ob(e){var t=e.getAttribute("style"),n=[];return t&&(n=t.split(";").reduce(function(r,a){var o=a.split(":"),i=o[0],s=o.slice(1);return i&&s.length>0&&(r[i]=s.join(":").trim()),r},{})),n}function Ib(e){var t=e.getAttribute("data-prefix"),n=e.getAttribute("data-icon"),r=e.innerText!==void 0?e.innerText.trim():"",a=qa(Ci(e));return a.prefix||(a.prefix=rn()),t&&n&&(a.prefix=t,a.iconName=n),a.iconName&&a.prefix||(a.prefix&&r.length>0&&(a.iconName=cb(a.prefix,e.innerText)||Ti(a.prefix,Ro(e.innerText))),!a.iconName&&Y.autoFetchSvg&&e.firstChild&&e.firstChild.nodeType===Node.TEXT_NODE&&(a.iconName=e.firstChild.data)),a}function Rb(e){var t=Gn(e.attributes).reduce(function(a,o){return a.name!=="class"&&a.name!=="style"&&(a[o.name]=o.value),a},{}),n=e.getAttribute("title"),r=e.getAttribute("data-fa-title-id");return Y.autoA11y&&(n?t["aria-labelledby"]="".concat(Y.replacementClass,"-title-").concat(r||xr()):(t["aria-hidden"]="true",t.focusable="false")),t}function Nb(){return{iconName:null,title:null,titleId:null,prefix:null,transform:kt,symbol:!1,mask:{iconName:null,prefix:null,rest:[]},maskId:null,extra:{classes:[],styles:{},attributes:{}}}}function dl(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{styleParser:!0},n=Ib(e),r=n.iconName,a=n.prefix,o=n.rest,i=Rb(e),s=Mo("parseNodeAttributes",{},e),l=t.styleParser?Ob(e):[];return K({iconName:r,title:e.getAttribute("title"),titleId:e.getAttribute("data-fa-title-id"),prefix:a,transform:kt,mask:{iconName:null,prefix:null,rest:[]},maskId:null,symbol:!1,extra:{classes:o,styles:l,attributes:i}},s)}var Mb=pt.styles;function $u(e){var t=Y.autoReplaceSvg==="nest"?dl(e,{styleParser:!1}):dl(e);return~t.extra.classes.indexOf(bu)?Mt("generateLayersText",e,t):Mt("generateSvgReplacementMutation",e,t)}var an=new Set;Ei.map(function(e){an.add("fa-".concat(e))});Object.keys(yr[Ee]).map(an.add.bind(an));Object.keys(yr[Te]).map(an.add.bind(an));an=Rr(an);function ml(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;if(!zt)return Promise.resolve();var n=Ce.documentElement.classList,r=function(f){return n.add("".concat(tl,"-").concat(f))},a=function(f){return n.remove("".concat(tl,"-").concat(f))},o=Y.autoFetchSvg?an:Ei.map(function(u){return"fa-".concat(u)}).concat(Object.keys(Mb));o.includes("fa")||o.push("fa");var i=[".".concat(bu,":not([").concat(_n,"])")].concat(o.map(function(u){return".".concat(u,":not([").concat(_n,"])")})).join(", ");if(i.length===0)return Promise.resolve();var s=[];try{s=Gn(e.querySelectorAll(i))}catch{}if(s.length>0)r("pending"),a("complete");else return Promise.resolve();var l=Oi.begin("onTree"),c=s.reduce(function(u,f){try{var d=$u(f);d&&u.push(d)}catch(h){gu||h.name==="MissingIcon"&&console.error(h)}return u},[]);return new Promise(function(u,f){Promise.all(c).then(function(d){Nu(d,function(){r("active"),r("complete"),a("pending"),typeof t=="function"&&t(),l(),u()})}).catch(function(d){l(),f(d)})})}function $b(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;$u(e).then(function(n){n&&Nu([n],t)})}function zb(e){return function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=(t||{}).icon?t:$o(t||{}),a=n.mask;return a&&(a=(a||{}).icon?a:$o(a||{})),e(r,K(K({},n),{},{mask:a}))}}var Db=function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=n.transform,a=r===void 0?kt:r,o=n.symbol,i=o===void 0?!1:o,s=n.mask,l=s===void 0?null:s,c=n.maskId,u=c===void 0?null:c,f=n.title,d=f===void 0?null:f,h=n.titleId,w=h===void 0?null:h,k=n.classes,E=k===void 0?[]:k,_=n.attributes,x=_===void 0?{}:_,v=n.styles,b=v===void 0?{}:v;if(t){var P=t.prefix,B=t.iconName,I=t.icon;return Wa(K({type:"icon"},t),function(){return wn("beforeDOMElementCreation",{iconDefinition:t,params:n}),Y.autoA11y&&(d?x["aria-labelledby"]="".concat(Y.replacementClass,"-title-").concat(w||xr()):(x["aria-hidden"]="true",x.focusable="false")),Pi({icons:{main:zo(I),mask:l?zo(l.icon):{found:!1,width:null,height:null,icon:{}}},prefix:P,iconName:B,transform:K(K({},kt),a),symbol:i,title:d,maskId:u,titleId:w,extra:{attributes:x,styles:b,classes:E}})})}},Hb={mixout:function(){return{icon:zb(Db)}},hooks:function(){return{mutationObserverCallbacks:function(n){return n.treeCallback=ml,n.nodeCallback=$b,n}}},provides:function(t){t.i2svg=function(n){var r=n.node,a=r===void 0?Ce:r,o=n.callback,i=o===void 0?function(){}:o;return ml(a,i)},t.generateSvgReplacementMutation=function(n,r){var a=r.iconName,o=r.title,i=r.titleId,s=r.prefix,l=r.transform,c=r.symbol,u=r.mask,f=r.maskId,d=r.extra;return new Promise(function(h,w){Promise.all([Do(a,s),u.iconName?Do(u.iconName,u.prefix):Promise.resolve({found:!1,width:512,height:512,icon:{}})]).then(function(k){var E=_i(k,2),_=E[0],x=E[1];h([n,Pi({icons:{main:_,mask:x},prefix:s,iconName:a,transform:l,symbol:c,maskId:f,title:o,titleId:i,extra:d,watchable:!0})])}).catch(w)})},t.generateAbstractIcon=function(n){var r=n.children,a=n.attributes,o=n.main,i=n.transform,s=n.styles,l=Ba(s);l.length>0&&(a.style=l);var c;return Si(i)&&(c=Mt("generateAbstractTransformGrouping",{main:o,transform:i,containerWidth:o.width,iconWidth:o.width})),r.push(c||o.icon),{children:r,attributes:a}}}},Fb={mixout:function(){return{layer:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},a=r.classes,o=a===void 0?[]:a;return Wa({type:"layer"},function(){wn("beforeDOMElementCreation",{assembler:n,params:r});var i=[];return n(function(s){Array.isArray(s)?s.map(function(l){i=i.concat(l.abstract)}):i=i.concat(s.abstract)}),[{tag:"span",attributes:{class:["".concat(Y.cssPrefix,"-layers")].concat(Rr(o)).join(" ")},children:i}]})}}}},jb={mixout:function(){return{counter:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},a=r.title,o=a===void 0?null:a,i=r.classes,s=i===void 0?[]:i,l=r.attributes,c=l===void 0?{}:l,u=r.styles,f=u===void 0?{}:u;return Wa({type:"counter",content:n},function(){return wn("beforeDOMElementCreation",{content:n,params:r}),_b({content:n.toString(),title:o,extra:{attributes:c,styles:f,classes:["".concat(Y.cssPrefix,"-layers-counter")].concat(Rr(s))}})})}}}},Bb={mixout:function(){return{text:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},a=r.transform,o=a===void 0?kt:a,i=r.title,s=i===void 0?null:i,l=r.classes,c=l===void 0?[]:l,u=r.attributes,f=u===void 0?{}:u,d=r.styles,h=d===void 0?{}:d;return Wa({type:"text",content:n},function(){return wn("beforeDOMElementCreation",{content:n,params:r}),sl({content:n,transform:K(K({},kt),o),title:s,extra:{attributes:f,styles:h,classes:["".concat(Y.cssPrefix,"-layers-text")].concat(Rr(c))}})})}}},provides:function(t){t.generateLayersText=function(n,r){var a=r.title,o=r.transform,i=r.extra,s=null,l=null;if(pu){var c=parseInt(getComputedStyle(n).fontSize,10),u=n.getBoundingClientRect();s=u.width/c,l=u.height/c}return Y.autoA11y&&!a&&(i.attributes["aria-hidden"]="true"),Promise.resolve([n,sl({content:n.innerHTML,width:s,height:l,transform:o,title:a,extra:i,watchable:!0})])}}},Ub=new RegExp('"',"ug"),pl=[1105920,1112319];function qb(e){var t=e.replace(Ub,""),n=rb(t,0),r=n>=pl[0]&&n<=pl[1],a=t.length===2?t[0]===t[1]:!1;return{value:Ro(a?t[0]:t),isSecondary:r||a}}function hl(e,t){var n="".concat(I1).concat(t.replace(":","-"));return new Promise(function(r,a){if(e.getAttribute(n)!==null)return r();var o=Gn(e.children),i=o.filter(function(I){return I.getAttribute(Io)===t})[0],s=nn.getComputedStyle(e,t),l=s.getPropertyValue("font-family").match(z1),c=s.getPropertyValue("font-weight"),u=s.getPropertyValue("content");if(i&&!l)return e.removeChild(i),r();if(l&&u!=="none"&&u!==""){var f=s.getPropertyValue("content"),d=~["Sharp"].indexOf(l[2])?Te:Ee,h=~["Solid","Regular","Light","Thin","Duotone","Brands","Kit"].indexOf(l[2])?_r[d][l[2].toLowerCase()]:D1[d][c],w=qb(f),k=w.value,E=w.isSecondary,_=l[0].startsWith("FontAwesome"),x=Ti(h,k),v=x;if(_){var b=ub(k);b.iconName&&b.prefix&&(x=b.iconName,h=b.prefix)}if(x&&!E&&(!i||i.getAttribute(ki)!==h||i.getAttribute(xi)!==v)){e.setAttribute(n,v),i&&e.removeChild(i);var P=Nb(),B=P.extra;B.attributes[Io]=t,Do(x,h).then(function(I){var g=Pi(K(K({},P),{},{icons:{main:I,mask:Li()},prefix:h,iconName:v,extra:B,watchable:!0})),M=Ce.createElementNS("http://www.w3.org/2000/svg","svg");t==="::before"?e.insertBefore(M,e.firstChild):e.appendChild(M),M.outerHTML=g.map(function(R){return Mr(R)}).join(` +`),e.removeAttribute(n),r()}).catch(a)}else r()}else r()})}function Wb(e){return Promise.all([hl(e,"::before"),hl(e,"::after")])}function Kb(e){return e.parentNode!==document.head&&!~N1.indexOf(e.tagName.toUpperCase())&&!e.getAttribute(Io)&&(!e.parentNode||e.parentNode.tagName!=="svg")}function vl(e){if(zt)return new Promise(function(t,n){var r=Gn(e.querySelectorAll("*")).filter(Kb).map(Wb),a=Oi.begin("searchPseudoElements");Mu(),Promise.all(r).then(function(){a(),Fo(),t()}).catch(function(){a(),Fo(),n()})})}var Vb={hooks:function(){return{mutationObserverCallbacks:function(n){return n.pseudoElementsCallback=vl,n}}},provides:function(t){t.pseudoElements2svg=function(n){var r=n.node,a=r===void 0?Ce:r;Y.searchPseudoElements&&vl(a)}}},gl=!1,Yb={mixout:function(){return{dom:{unwatch:function(){Mu(),gl=!0}}}},hooks:function(){return{bootstrap:function(){fl(Mo("mutationObserverCallbacks",{}))},noAuto:function(){Pb()},watch:function(n){var r=n.observeMutationsRoot;gl?Fo():fl(Mo("mutationObserverCallbacks",{observeMutationsRoot:r}))}}}},bl=function(t){var n={size:16,x:0,y:0,flipX:!1,flipY:!1,rotate:0};return t.toLowerCase().split(" ").reduce(function(r,a){var o=a.toLowerCase().split("-"),i=o[0],s=o.slice(1).join("-");if(i&&s==="h")return r.flipX=!0,r;if(i&&s==="v")return r.flipY=!0,r;if(s=parseFloat(s),isNaN(s))return r;switch(i){case"grow":r.size=r.size+s;break;case"shrink":r.size=r.size-s;break;case"left":r.x=r.x-s;break;case"right":r.x=r.x+s;break;case"up":r.y=r.y-s;break;case"down":r.y=r.y+s;break;case"rotate":r.rotate=r.rotate+s;break}return r},n)},Gb={mixout:function(){return{parse:{transform:function(n){return bl(n)}}}},hooks:function(){return{parseNodeAttributes:function(n,r){var a=r.getAttribute("data-fa-transform");return a&&(n.transform=bl(a)),n}}},provides:function(t){t.generateAbstractTransformGrouping=function(n){var r=n.main,a=n.transform,o=n.containerWidth,i=n.iconWidth,s={transform:"translate(".concat(o/2," 256)")},l="translate(".concat(a.x*32,", ").concat(a.y*32,") "),c="scale(".concat(a.size/16*(a.flipX?-1:1),", ").concat(a.size/16*(a.flipY?-1:1),") "),u="rotate(".concat(a.rotate," 0 0)"),f={transform:"".concat(l," ").concat(c," ").concat(u)},d={transform:"translate(".concat(i/2*-1," -256)")},h={outer:s,inner:f,path:d};return{tag:"g",attributes:K({},h.outer),children:[{tag:"g",attributes:K({},h.inner),children:[{tag:r.icon.tag,children:r.icon.children,attributes:K(K({},r.icon.attributes),h.path)}]}]}}}},fo={x:0,y:0,width:"100%",height:"100%"};function yl(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return e.attributes&&(e.attributes.fill||t)&&(e.attributes.fill="black"),e}function Xb(e){return e.tag==="g"?e.children:[e]}var Jb={hooks:function(){return{parseNodeAttributes:function(n,r){var a=r.getAttribute("data-fa-mask"),o=a?qa(a.split(" ").map(function(i){return i.trim()})):Li();return o.prefix||(o.prefix=rn()),n.mask=o,n.maskId=r.getAttribute("data-fa-mask-id"),n}}},provides:function(t){t.generateAbstractMask=function(n){var r=n.children,a=n.attributes,o=n.main,i=n.mask,s=n.maskId,l=n.transform,c=o.width,u=o.icon,f=i.width,d=i.icon,h=G1({transform:l,containerWidth:f,iconWidth:c}),w={tag:"rect",attributes:K(K({},fo),{},{fill:"white"})},k=u.children?{children:u.children.map(yl)}:{},E={tag:"g",attributes:K({},h.inner),children:[yl(K({tag:u.tag,attributes:K(K({},u.attributes),h.path)},k))]},_={tag:"g",attributes:K({},h.outer),children:[E]},x="mask-".concat(s||xr()),v="clip-".concat(s||xr()),b={tag:"mask",attributes:K(K({},fo),{},{id:x,maskUnits:"userSpaceOnUse",maskContentUnits:"userSpaceOnUse"}),children:[w,_]},P={tag:"defs",children:[{tag:"clipPath",attributes:{id:v},children:Xb(d)},b]};return r.push(P,{tag:"rect",attributes:K({fill:"currentColor","clip-path":"url(#".concat(v,")"),mask:"url(#".concat(x,")")},fo)}),{children:r,attributes:a}}}},Qb={provides:function(t){var n=!1;nn.matchMedia&&(n=nn.matchMedia("(prefers-reduced-motion: reduce)").matches),t.missingIconAbstract=function(){var r=[],a={fill:"currentColor"},o={attributeType:"XML",repeatCount:"indefinite",dur:"2s"};r.push({tag:"path",attributes:K(K({},a),{},{d:"M156.5,447.7l-12.6,29.5c-18.7-9.5-35.9-21.2-51.5-34.9l22.7-22.7C127.6,430.5,141.5,440,156.5,447.7z M40.6,272H8.5 c1.4,21.2,5.4,41.7,11.7,61.1L50,321.2C45.1,305.5,41.8,289,40.6,272z M40.6,240c1.4-18.8,5.2-37,11.1-54.1l-29.5-12.6 C14.7,194.3,10,216.7,8.5,240H40.6z M64.3,156.5c7.8-14.9,17.2-28.8,28.1-41.5L69.7,92.3c-13.7,15.6-25.5,32.8-34.9,51.5 L64.3,156.5z M397,419.6c-13.9,12-29.4,22.3-46.1,30.4l11.9,29.8c20.7-9.9,39.8-22.6,56.9-37.6L397,419.6z M115,92.4 c13.9-12,29.4-22.3,46.1-30.4l-11.9-29.8c-20.7,9.9-39.8,22.6-56.8,37.6L115,92.4z M447.7,355.5c-7.8,14.9-17.2,28.8-28.1,41.5 l22.7,22.7c13.7-15.6,25.5-32.9,34.9-51.5L447.7,355.5z M471.4,272c-1.4,18.8-5.2,37-11.1,54.1l29.5,12.6 c7.5-21.1,12.2-43.5,13.6-66.8H471.4z M321.2,462c-15.7,5-32.2,8.2-49.2,9.4v32.1c21.2-1.4,41.7-5.4,61.1-11.7L321.2,462z M240,471.4c-18.8-1.4-37-5.2-54.1-11.1l-12.6,29.5c21.1,7.5,43.5,12.2,66.8,13.6V471.4z M462,190.8c5,15.7,8.2,32.2,9.4,49.2h32.1 c-1.4-21.2-5.4-41.7-11.7-61.1L462,190.8z M92.4,397c-12-13.9-22.3-29.4-30.4-46.1l-29.8,11.9c9.9,20.7,22.6,39.8,37.6,56.9 L92.4,397z M272,40.6c18.8,1.4,36.9,5.2,54.1,11.1l12.6-29.5C317.7,14.7,295.3,10,272,8.5V40.6z M190.8,50 c15.7-5,32.2-8.2,49.2-9.4V8.5c-21.2,1.4-41.7,5.4-61.1,11.7L190.8,50z M442.3,92.3L419.6,115c12,13.9,22.3,29.4,30.5,46.1 l29.8-11.9C470,128.5,457.3,109.4,442.3,92.3z M397,92.4l22.7-22.7c-15.6-13.7-32.8-25.5-51.5-34.9l-12.6,29.5 C370.4,72.1,384.4,81.5,397,92.4z"})});var i=K(K({},o),{},{attributeName:"opacity"}),s={tag:"circle",attributes:K(K({},a),{},{cx:"256",cy:"364",r:"28"}),children:[]};return n||s.children.push({tag:"animate",attributes:K(K({},o),{},{attributeName:"r",values:"28;14;28;28;14;28;"})},{tag:"animate",attributes:K(K({},i),{},{values:"1;0;1;1;0;1;"})}),r.push(s),r.push({tag:"path",attributes:K(K({},a),{},{opacity:"1",d:"M263.7,312h-16c-6.6,0-12-5.4-12-12c0-71,77.4-63.9,77.4-107.8c0-20-17.8-40.2-57.4-40.2c-29.1,0-44.3,9.6-59.2,28.7 c-3.9,5-11.1,6-16.2,2.4l-13.1-9.2c-5.6-3.9-6.9-11.8-2.6-17.2c21.2-27.2,46.4-44.7,91.2-44.7c52.3,0,97.4,29.8,97.4,80.2 c0,67.6-77.4,63.5-77.4,107.8C275.7,306.6,270.3,312,263.7,312z"}),children:n?[]:[{tag:"animate",attributes:K(K({},i),{},{values:"1;0;0;0;0;1;"})}]}),n||r.push({tag:"path",attributes:K(K({},a),{},{opacity:"0",d:"M232.5,134.5l7,168c0.3,6.4,5.6,11.5,12,11.5h9c6.4,0,11.7-5.1,12-11.5l7-168c0.3-6.8-5.2-12.5-12-12.5h-23 C237.7,122,232.2,127.7,232.5,134.5z"}),children:[{tag:"animate",attributes:K(K({},i),{},{values:"0;0;1;1;0;0;"})}]}),{tag:"g",attributes:{class:"missing"},children:r}}}},Zb={hooks:function(){return{parseNodeAttributes:function(n,r){var a=r.getAttribute("data-fa-symbol"),o=a===null?!1:a===""?!0:a;return n.symbol=o,n}}}},e0=[Q1,Hb,Fb,jb,Bb,Vb,Yb,Gb,Jb,Qb,Zb];mb(e0,{mixoutsTo:rt});rt.noAuto;rt.config;var t0=rt.library;rt.dom;var jo=rt.parse;rt.findIconDefinition;rt.toHtml;var n0=rt.icon;rt.layer;rt.text;rt.counter;var r0={prefix:"fab",iconName:"discord",icon:[640,512,[],"f392","M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"]},a0={prefix:"fab",iconName:"linkedin",icon:[448,512,[],"f08c","M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"]},o0={prefix:"fab",iconName:"github",icon:[496,512,[],"f09b","M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"]},i0={prefix:"fab",iconName:"twitter",icon:[512,512,[],"f099","M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"]},s0={prefix:"fas",iconName:"bullhorn",icon:[512,512,[128226,128363],"f0a1","M480 32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9L381.7 53c-48 48-113.1 75-181 75H192 160 64c-35.3 0-64 28.7-64 64v96c0 35.3 28.7 64 64 64l0 128c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V352l8.7 0c67.9 0 133 27 181 75l43.6 43.6c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V300.4c18.6-8.8 32-32.5 32-60.4s-13.4-51.6-32-60.4V32zm-64 76.7V240 371.3C357.2 317.8 280.5 288 200.7 288H192V192h8.7c79.8 0 156.5-29.8 215.3-83.3z"]},l0={prefix:"fas",iconName:"globe",icon:[512,512,[127760],"f0ac","M352 256c0 22.2-1.2 43.6-3.3 64H163.3c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64H348.7c2.2 20.4 3.3 41.8 3.3 64zm28.8-64H503.9c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64H380.8c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32H376.7c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0H167.7c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0H18.6C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192H131.2c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64H8.1C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6H344.3c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352H135.3zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6H493.4z"]},c0={prefix:"fas",iconName:"chevron-left",icon:[320,512,[9001],"f053","M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"]},u0={prefix:"fas",iconName:"chevron-right",icon:[320,512,[9002],"f054","M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"]};function _l(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 Pt(e){for(var t=1;t=0)&&(n[a]=e[a]);return n}function d0(e,t){if(e==null)return{};var n=f0(e,t),r,a;if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}var m0=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},zu={exports:{}};(function(e){(function(t){var n=function(_,x,v){if(!c(x)||f(x)||d(x)||h(x)||l(x))return x;var b,P=0,B=0;if(u(x))for(b=[],B=x.length;P1&&arguments[1]!==void 0?arguments[1]:{},n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};if(typeof e=="string")return e;var r=(e.children||[]).map(function(l){return Du(l)}),a=Object.keys(e.attributes||{}).reduce(function(l,c){var u=e.attributes[c];switch(c){case"class":l.class=g0(u);break;case"style":l.style=v0(u);break;default:l.attrs[c]=u}return l},{attrs:{},class:{},style:{}});n.class;var o=n.style,i=o===void 0?{}:o,s=d0(n,h0);return ue(e.tag,Pt(Pt(Pt({},t),{},{class:a.class,style:Pt(Pt({},a.style),i)},a.attrs),s),r)}var Hu=!1;try{Hu=!0}catch{}function b0(){if(!Hu&&console&&typeof console.error=="function"){var e;(e=console).error.apply(e,arguments)}}function mo(e,t){return Array.isArray(t)&&t.length>0||!Array.isArray(t)&&t?Xe({},e,t):{}}function y0(e){var t,n=(t={"fa-spin":e.spin,"fa-pulse":e.pulse,"fa-fw":e.fixedWidth,"fa-border":e.border,"fa-li":e.listItem,"fa-inverse":e.inverse,"fa-flip":e.flip===!0,"fa-flip-horizontal":e.flip==="horizontal"||e.flip==="both","fa-flip-vertical":e.flip==="vertical"||e.flip==="both"},Xe(t,"fa-".concat(e.size),e.size!==null),Xe(t,"fa-rotate-".concat(e.rotation),e.rotation!==null),Xe(t,"fa-pull-".concat(e.pull),e.pull!==null),Xe(t,"fa-swap-opacity",e.swapOpacity),Xe(t,"fa-bounce",e.bounce),Xe(t,"fa-shake",e.shake),Xe(t,"fa-beat",e.beat),Xe(t,"fa-fade",e.fade),Xe(t,"fa-beat-fade",e.beatFade),Xe(t,"fa-flash",e.flash),Xe(t,"fa-spin-pulse",e.spinPulse),Xe(t,"fa-spin-reverse",e.spinReverse),t);return Object.keys(n).map(function(r){return n[r]?r:null}).filter(function(r){return r})}function wl(e){if(e&&Sa(e)==="object"&&e.prefix&&e.iconName&&e.icon)return e;if(jo.icon)return jo.icon(e);if(e===null)return null;if(Sa(e)==="object"&&e.prefix&&e.iconName)return e;if(Array.isArray(e)&&e.length===2)return{prefix:e[0],iconName:e[1]};if(typeof e=="string")return{prefix:"fas",iconName:e}}var _0=fe({name:"FontAwesomeIcon",props:{border:{type:Boolean,default:!1},fixedWidth:{type:Boolean,default:!1},flip:{type:[Boolean,String],default:!1,validator:function(t){return[!0,!1,"horizontal","vertical","both"].indexOf(t)>-1}},icon:{type:[Object,Array,String],required:!0},mask:{type:[Object,Array,String],default:null},listItem:{type:Boolean,default:!1},pull:{type:String,default:null,validator:function(t){return["right","left"].indexOf(t)>-1}},pulse:{type:Boolean,default:!1},rotation:{type:[String,Number],default:null,validator:function(t){return[90,180,270].indexOf(Number.parseInt(t,10))>-1}},swapOpacity:{type:Boolean,default:!1},size:{type:String,default:null,validator:function(t){return["2xs","xs","sm","lg","xl","2xl","1x","2x","3x","4x","5x","6x","7x","8x","9x","10x"].indexOf(t)>-1}},spin:{type:Boolean,default:!1},transform:{type:[String,Object],default:null},symbol:{type:[Boolean,String],default:!1},title:{type:String,default:null},inverse:{type:Boolean,default:!1},bounce:{type:Boolean,default:!1},shake:{type:Boolean,default:!1},beat:{type:Boolean,default:!1},fade:{type:Boolean,default:!1},beatFade:{type:Boolean,default:!1},flash:{type:Boolean,default:!1},spinPulse:{type:Boolean,default:!1},spinReverse:{type:Boolean,default:!1}},setup:function(t,n){var r=n.attrs,a=D(function(){return wl(t.icon)}),o=D(function(){return mo("classes",y0(t))}),i=D(function(){return mo("transform",typeof t.transform=="string"?jo.transform(t.transform):t.transform)}),s=D(function(){return mo("mask",wl(t.mask))}),l=D(function(){return n0(a.value,Pt(Pt(Pt(Pt({},o.value),i.value),s.value),{},{symbol:t.symbol,title:t.title}))});lt(l,function(u){if(!u)return b0("Could not find one or more icon(s)",a.value,s.value)},{immediate:!0});var c=D(function(){return l.value?Du(l.value.abstract[0],{},r):null});return function(){return c.value}}});t0.add(r0,o0,i0,a0,l0,s0,c0,u0);const w0=gt({enhance({app:e}){e.component("font-awesome-icon",_0)}}),aa=[$p,Hp,Up,Vp,Qp,Ng,Gg,Xg,Jg,c1,u1,k1,w0],k0=[["v-8daa1a0e","/",{title:""},["/index.md"]],["v-7e348068","/testimonials.html",{title:""},[":md"]],["v-fffb8e28","/guide/",{title:"User guide"},["/guide/index.md"]],["v-5c82f7c0","/motivation/",{title:"Motivation"},["/motivation/index.md"]],["v-73866439","/support/",{title:"Support"},["/support/index.md"]],["v-3706649a","/404.html",{title:""},[]]];var kl=fe({name:"Vuepress",setup(){const e=Em();return()=>ue(e.value)}}),x0=()=>k0.reduce((e,[t,n,r,a])=>(e.push({name:t,path:n,component:kl,meta:r},{path:n.endsWith("/")?n+"index.html":n.substring(0,n.length-5),redirect:n},...a.map(o=>({path:o===":md"?n.substring(0,n.length-5)+".md":o,redirect:n}))),e),[{name:"404",path:"/:catchAll(.*)",component:kl}]),E0=Km,C0=()=>{const e=Lp({history:E0(xc("/jmeter-java-dsl/")),routes:x0(),scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{var r;(t.path!==n.path||n===Tt)&&([Vt.value]=await Promise.all([At.resolvePageData(t.name),(r=Sc[t.name])==null?void 0:r.__asyncLoader()]))}),e},S0=e=>{e.component("ClientOnly",Ha),e.component("Content",Tm)},A0=(e,t,n)=>{const r=D(()=>At.resolveLayouts(n)),a=js(()=>t.currentRoute.value.path),o=js(()=>At.resolveRouteLocale(Ln.value.locales,a.value)),i=D(()=>At.resolveSiteLocaleData(Ln.value,o.value)),s=D(()=>At.resolvePageFrontmatter(Vt.value)),l=D(()=>At.resolvePageHeadTitle(Vt.value,i.value)),c=D(()=>At.resolvePageHead(l.value,s.value,i.value)),u=D(()=>At.resolvePageLang(Vt.value)),f=D(()=>At.resolvePageLayout(Vt.value,r.value));return e.provide(ym,r),e.provide(Tc,s),e.provide(km,l),e.provide(Lc,c),e.provide(Pc,u),e.provide(Oc,f),e.provide(li,o),e.provide(Rc,i),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>s.value},$head:{get:()=>c.value},$headTitle:{get:()=>l.value},$lang:{get:()=>u.value},$page:{get:()=>Vt.value},$routeLocale:{get:()=>o.value},$site:{get:()=>Ln.value},$siteLocale:{get:()=>i.value},$withBase:{get:()=>ui}}),{layouts:r,pageData:Vt,pageFrontmatter:s,pageHead:c,pageHeadTitle:l,pageLang:u,pageLayout:f,routeLocale:o,siteData:Ln,siteLocaleData:i}},T0=()=>{const e=wm(),t=xm(),n=ve([]),r=()=>{e.value.forEach(o=>{const i=L0(o);i&&n.value.push(i)})},a=()=>{document.documentElement.lang=t.value,n.value.forEach(o=>{o.parentNode===document.head&&document.head.removeChild(o)}),n.value.splice(0,n.value.length),e.value.forEach(o=>{const i=P0(o);i!==null&&(document.head.appendChild(i),n.value.push(i))})};gn(Cm,a),Ye(()=>{r(),a(),lt(()=>e.value,a)})},L0=([e,t,n=""])=>{const r=Object.entries(t).map(([s,l])=>ge(l)?`[${s}=${JSON.stringify(l)}]`:l===!0?`[${s}]`:"").join(""),a=`head > ${e}${r}`;return Array.from(document.querySelectorAll(a)).find(s=>s.innerText===n)||null},P0=([e,t,n])=>{if(!ge(e))return null;const r=document.createElement(e);return si(t)&&Object.entries(t).forEach(([a,o])=>{ge(o)?r.setAttribute(a,o):o===!0&&r.setAttribute(a,"")}),ge(n)&&r.appendChild(document.createTextNode(n)),r},O0=fm,I0=async()=>{var n;const e=O0({name:"VuepressApp",setup(){var r;T0();for(const a of aa)(r=a.setup)==null||r.call(a);return()=>[ue(Wc),...aa.flatMap(({rootComponents:a=[]})=>a.map(o=>ue(o)))]}}),t=C0();S0(e),A0(e,t,aa);for(const r of aa)await((n=r.enhance)==null?void 0:n.call(r,{app:e,router:t,siteData:Ln}));return e.use(t),{app:e,router:t}};I0().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{Ha as C,Ae as _,le as a,Ct as b,ne as c,I0 as createVueApp,re as d,Sd as e,fe as f,ve as g,Oe as h,ke as i,D as j,Ke as k,Qe as n,q as o,xt as r,Ia as t,Q as u,De as w}; diff --git a/assets/azure-35125246.png b/assets/azure-35125246.png new file mode 100644 index 00000000..74a685af Binary files /dev/null and b/assets/azure-35125246.png differ diff --git a/assets/azure-logo-1bad44c3.png b/assets/azure-logo-1bad44c3.png new file mode 100644 index 00000000..915566d7 Binary files /dev/null and b/assets/azure-logo-1bad44c3.png differ diff --git a/assets/back-to-top-8efcbe56.svg b/assets/back-to-top-8efcbe56.svg new file mode 100644 index 00000000..83236781 --- /dev/null +++ b/assets/back-to-top-8efcbe56.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/blazemeter-85d7b816.png b/assets/blazemeter-85d7b816.png new file mode 100644 index 00000000..dd86cec3 Binary files /dev/null and b/assets/blazemeter-85d7b816.png differ diff --git a/assets/blazemeter-logo-a5731ee5.png b/assets/blazemeter-logo-a5731ee5.png new file mode 100644 index 00000000..75cfe78b Binary files /dev/null and b/assets/blazemeter-logo-a5731ee5.png differ diff --git a/assets/config-ide-autocomplete-230365f9.png b/assets/config-ide-autocomplete-230365f9.png new file mode 100644 index 00000000..44302999 Binary files /dev/null and b/assets/config-ide-autocomplete-230365f9.png differ diff --git a/assets/dashboard-777064cf.png b/assets/dashboard-777064cf.png new file mode 100644 index 00000000..b9528757 Binary files /dev/null and b/assets/dashboard-777064cf.png differ diff --git a/assets/datadog-3aaf2aae.png b/assets/datadog-3aaf2aae.png new file mode 100644 index 00000000..2c7e828d Binary files /dev/null and b/assets/datadog-3aaf2aae.png differ diff --git a/assets/grafana-ee0c26ae.png b/assets/grafana-ee0c26ae.png new file mode 100644 index 00000000..491ee8ff Binary files /dev/null and b/assets/grafana-ee0c26ae.png differ diff --git a/assets/index.html-0b2e03e0.js b/assets/index.html-0b2e03e0.js new file mode 100644 index 00000000..80a0f741 --- /dev/null +++ b/assets/index.html-0b2e03e0.js @@ -0,0 +1,27 @@ +import{_ as o,r as i,o as p,c as r,a as n,b as a,d as t,e}from"./app-863e42f0.js";const l={},c=n("h1",{id:"motivation",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#motivation","aria-hidden":"true"},"#"),a(" Motivation")],-1),u={href:"http://jmeter.apache.org/",target:"_blank",rel:"noopener noreferrer"},d={href:"https://gatling.io/",target:"_blank",rel:"noopener noreferrer"},m=n("p",null,"Here we explore some alternatives, their pros & cons, and the main motivations behind the development of jmeter-java-dsl.",-1),k=n("h2",{id:"alternatives-analysis",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#alternatives-analysis","aria-hidden":"true"},"#"),a(" Alternatives analysis")],-1),h=n("h3",{id:"jmeter",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#jmeter","aria-hidden":"true"},"#"),a(" JMeter")],-1),b=n("p",null,"JMeter is great for people with no programming knowledge since it provides a graphical interface to create test plans and run them. Additionally, it is the most popular tool (with a lot of supporting tools built on it) and has a big amount of supported protocols and plugins making it very versatile.",-1),v={href:"https://www.blazemeter.com/blog/5-ways-launch-jmeter-test-without-using-jmeter-gui/",target:"_blank",rel:"noopener noreferrer"},g=e(`
import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import org.apache.http.entity.ContentType;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+      threadGroup(2, 10,
+        httpSampler("http://my.service")
+          .post("{\\"name\\": \\"test\\"}", ContentType.APPLICATION_JSON)
+      ),
+      //this is just to log details of each request stats
+      jtlWriter("target/jtls")
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+  
+}
+
`,1),f={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/docs/motivation/sample.jmx",target:"_blank",rel:"noopener noreferrer"},y=e('

Gatling

Gatling does provide a simple API and Git-friendly format but requires scala knowledge and environment [1]. Additionally, it doesn't provide as a rich environment as JMeter (protocol support, plugins, tools) and requires learning a new framework for testing (if you already use JMeter, which is the most popular tool).

Taurus

',3),w={href:"https://gettaurus.org/",target:"_blank",rel:"noopener noreferrer"},_=n("h3",{id:"ruby-dsl",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#ruby-dsl","aria-hidden":"true"},"#"),a(" ruby-dsl")],-1),j={href:"https://github.com/flood-io/ruby-jmeter",target:"_blank",rel:"noopener noreferrer"},J=e('

jmeter-java-dsl

jmeter-java-dsl tries to get the best of these tools by providing a simple java API with Git friendly format to run JMeter tests, taking advantage of all JMeter benefits and knowledge and also providing many of the benefits of Gatling scripting. As shown in the previous example, it can be easily executed with JUnit, modularized in code, and easily integrated into any CI/CD pipeline. Additionally, it makes it easy to debug the execution of test plans with the usual IDE debugger tools. Finally, as with most Java libraries, you can use it not only in a Java project but also in projects of most JVM languages (like kotlin, scala, groovy, etc.).

Comparison Table

Here is a table with a summary of the main pros and cons of each tool:

ToolProsCons
JMeter👍 GUI for non programmers
👍 Popularity
👍 Protocols Support
👍 Documentation
👍 Rich ecosystem
👎 Slow test plan creation
👎 No VCS friendly format
👎 Not programmers friendly
👎 No simple CI/CD integration
Gatling👍 VCS friendly
👍 IDE friendly (auto-complete and debug)
👍 Natural CI/CD integration
👍 Natural code modularization and reuse
👍 Less resources (CPU & RAM) usage
👍 All details of simple test plans at a glance
👍 Simple way to do assertions on statistics
👎 Scala knowledge and environment required [1]
👎 Smaller set of protocols supported
👎 Less documentation & tooling
👎 Live statistics charts & grafana integration only available in enterprise version
Taurus👍 VCS friendly
👍 Simple CI/CD integration
👍 Unified framework for running any type of test
👍 built-in support for running tests at scale
👍 All details of simple test plans at a glance
👍 Simple way to do assertions on statistics
👎 Both Java and Python environments required
👎 Not as simple to discover (IDE auto-complete or GUI) supported functionality
👎 Not complete support of JMeter capabilities (nor in the roadmap)
ruby-dsl👍 VCS friendly
👍 Simple CI/CD integration
👍 Unified framework for running any type of test
👍 built-in support for running tests at scale
👍 All details of simple test plans at a glance
👎 Both Java and Ruby environments required
👎 Not following same naming convention and structure as JMeter
👎 Not complete support of JMeter capabilities (nor in the roadmap)
👎 No integration for debugging JMeter code
jmeter-java-dsl👍 VCS friendly
👍 IDE friendly (auto-complete and debug)
👍 Natural CI/CD integration
👍 Natural code modularization and reuse
👍 Existing JMeter documentation
👍 Easy to add support for JMeter supported protocols and new plugins
👍 Could easily interact with JMX files and take advantage of JMeter ecosystem
👍 All details of simple test plans at a glance
👍 Simple way to do assertions on statistics
👎 Basic Java knowledge required
👎 Same resources (CPU & RAM) usage as JMeter
',5),x={class:"custom-container tip"},M=n("p",{class:"custom-container-title"},"Notes",-1),I=n("a",{name:"gatling-java"},null,-1),C={href:"https://gatling.io/2021/11/gatling-3-7-java-dsl-kotlin-and-much-more/",target:"_blank",rel:"noopener noreferrer"},S=n("p",null,[a("As a side note, take into consideration that the underlying code is still Scala and async model-based, which makes debugging and understanding it harder for Java developers than JMeter code. Additionally, the model is still tied to "),n("code",null,"Simulator"),a(" classes and maven (gradle or sbt) plugin to be able to run the tests, compared to the simplicity and flexibility of jmeter-java-dsl tests execution.")],-1);function A(T,N){const s=i("ExternalLinkIcon");return p(),r("div",null,[c,n("p",null,[a("There are many tools to script performance/load tests, being "),n("a",u,[a("JMeter"),t(s)]),a(" and "),n("a",d,[a("Gatling"),t(s)]),a(" the most popular ones.")]),m,k,h,b,n("p",null,[a("But, JMeter has some downsides as well: sometimes it might be slow to create test plans in JMeter GUI and you can't get the full picture of the test plan unless you dig in every tree node to check its properties. Furthermore, it doesn't provide a simple programmer-friendly API (you can check "),n("a",v,[a("here"),t(s)]),a(" for an example of how to run JMeter programmatically without jmeter-java-dsl), nor a Git-friendly format (too verbose and hard to review). For example, for this test plan:")]),g,n("p",null,[a("In JMeter, you would need a JMX file like "),n("a",f,[a("this"),t(s)]),a(", and even then, it wouldn't be as simple to do assertions on collected statistics as in provided example.")]),y,n("p",null,[n("a",w,[a("Taurus"),t(s)]),a(" is another open-source tool that allows specifying tests in a Git-friendly yaml syntax, and provides additional features like pass/fail criteria and easier CI/CD integration. But, this tool requires a python environment, in addition to the java environment. Additionally, there is no built-in GUI or IDE auto-completion support, which makes it harder to discover and learn the actual syntax. Finally, Taurus syntax only supports a subset of the features JMeter provides.")]),_,n("p",null,[a("Finally, "),n("a",j,[a("ruby-dsl"),t(s)]),a(" is also an open-source library that allows specifying and running in ruby custom DSL JMeter test plans. This is the most similar tool to jmeter-java-dsl, but it requires ruby (in addition to the java environment) with the additional performance impact, does not follow the same naming and structure convention as JMeter, and lacks debugging integration with JMeter execution engine.")]),J,n("div",x,[M,n("ol",null,[n("li",null,[n("p",null,[I,a(" One year after jmeter-java-dsl release, on November 2021, Gatling released "),n("a",C,[a("3.7 version"),t(s)]),a(", including a Java friendly API for existing Gatling Scala API. This greatly simplifies usage for Java users and is a great addition to Gatling.")]),S])])])])}const P=o(l,[["render",A],["__file","index.html.vue"]]);export{P as default}; diff --git a/assets/index.html-0e290538.js b/assets/index.html-0e290538.js new file mode 100644 index 00000000..c3ce965f --- /dev/null +++ b/assets/index.html-0e290538.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-5c82f7c0","path":"/motivation/","title":"Motivation","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"Alternatives analysis","slug":"alternatives-analysis","link":"#alternatives-analysis","children":[{"level":3,"title":"JMeter","slug":"jmeter","link":"#jmeter","children":[]},{"level":3,"title":"Gatling","slug":"gatling","link":"#gatling","children":[]},{"level":3,"title":"Taurus","slug":"taurus","link":"#taurus","children":[]},{"level":3,"title":"ruby-dsl","slug":"ruby-dsl","link":"#ruby-dsl","children":[]},{"level":3,"title":"jmeter-java-dsl","slug":"jmeter-java-dsl","link":"#jmeter-java-dsl","children":[]}]},{"level":2,"title":"Comparison Table","slug":"comparison-table","link":"#comparison-table","children":[]}],"git":{},"filePathRelative":"motivation/index.md"}');export{l as data}; diff --git a/assets/index.html-20640c9f.js b/assets/index.html-20640c9f.js new file mode 100644 index 00000000..36d6b243 --- /dev/null +++ b/assets/index.html-20640c9f.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-73866439","path":"/support/","title":"Support","lang":"en-US","frontmatter":{"sidebar":false},"headers":[{"level":2,"title":"Community Support","slug":"community-support","link":"#community-support","children":[]},{"level":2,"title":"Enterprise Support by Abstracta","slug":"enterprise-support-by-abstracta","link":"#enterprise-support-by-abstracta","children":[]},{"level":2,"title":"Industry Support","slug":"industry-support","link":"#industry-support","children":[]}],"git":{},"filePathRelative":"support/index.md"}');export{t as data}; diff --git a/assets/index.html-2126a2d0.js b/assets/index.html-2126a2d0.js new file mode 100644 index 00000000..2a2a7b85 --- /dev/null +++ b/assets/index.html-2126a2d0.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-fffb8e28","path":"/guide/","title":"User guide","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"Setup","slug":"setup","link":"#setup","children":[]},{"level":2,"title":"Simple HTTP test plan","slug":"simple-http-test-plan","link":"#simple-http-test-plan","children":[]},{"level":2,"title":"DSL recorder","slug":"dsl-recorder","link":"#dsl-recorder","children":[{"level":3,"title":"Correlations","slug":"correlations","link":"#correlations","children":[]}]},{"level":2,"title":"DSL code generation from JMX file","slug":"dsl-code-generation-from-jmx-file","link":"#dsl-code-generation-from-jmx-file","children":[]},{"level":2,"title":"Run test at scale","slug":"run-test-at-scale","link":"#run-test-at-scale","children":[{"level":3,"title":"BlazeMeter","slug":"blazemeter","link":"#blazemeter","children":[]},{"level":3,"title":"OctoPerf","slug":"octoperf","link":"#octoperf","children":[]},{"level":3,"title":"Azure Load Testing","slug":"azure-load-testing","link":"#azure-load-testing","children":[]},{"level":3,"title":"JMeter remote testing","slug":"jmeter-remote-testing","link":"#jmeter-remote-testing","children":[]}]},{"level":2,"title":"Auto Stop","slug":"auto-stop","link":"#auto-stop","children":[]},{"level":2,"title":"Advanced threads configuration","slug":"advanced-threads-configuration","link":"#advanced-threads-configuration","children":[{"level":3,"title":"Thread ramps and holds","slug":"thread-ramps-and-holds","link":"#thread-ramps-and-holds","children":[]},{"level":3,"title":"Throughput based thread group","slug":"throughput-based-thread-group","link":"#throughput-based-thread-group","children":[]},{"level":3,"title":"Set up & tear down","slug":"set-up-tear-down","link":"#set-up-tear-down","children":[]},{"level":3,"title":"Thread groups order","slug":"thread-groups-order","link":"#thread-groups-order","children":[]}]},{"level":2,"title":"Test plan debugging","slug":"test-plan-debugging","link":"#test-plan-debugging","children":[{"level":3,"title":"View results tree","slug":"view-results-tree","link":"#view-results-tree","children":[]},{"level":3,"title":"Post-processor breakpoints","slug":"post-processor-breakpoints","link":"#post-processor-breakpoints","children":[]},{"level":3,"title":"Debug info during test plan execution","slug":"debug-info-during-test-plan-execution","link":"#debug-info-during-test-plan-execution","children":[]},{"level":3,"title":"Debug JMeter code","slug":"debug-jmeter-code","link":"#debug-jmeter-code","children":[]},{"level":3,"title":"Debug Groovy code","slug":"debug-groovy-code","link":"#debug-groovy-code","children":[]},{"level":3,"title":"Dummy sampler","slug":"dummy-sampler","link":"#dummy-sampler","children":[]},{"level":3,"title":"Test plan review in JMeter GUI","slug":"test-plan-review-in-jmeter-gui","link":"#test-plan-review-in-jmeter-gui","children":[]}]},{"level":2,"title":"Reporting","slug":"reporting","link":"#reporting","children":[{"level":3,"title":"Log requests and responses","slug":"log-requests-and-responses","link":"#log-requests-and-responses","children":[]},{"level":3,"title":"Real-time metrics visualization and historic data storage","slug":"real-time-metrics-visualization-and-historic-data-storage","link":"#real-time-metrics-visualization-and-historic-data-storage","children":[{"level":4,"title":"InfluxDB","slug":"influxdb","link":"#influxdb","children":[]},{"level":4,"title":"Graphite","slug":"graphite","link":"#graphite","children":[]},{"level":4,"title":"Elasticsearch","slug":"elasticsearch","link":"#elasticsearch","children":[]},{"level":4,"title":"Prometheus","slug":"prometheus","link":"#prometheus","children":[]},{"level":4,"title":"DataDog","slug":"datadog","link":"#datadog","children":[]}]},{"level":3,"title":"Generate HTML reports from test plan execution","slug":"generate-html-reports-from-test-plan-execution","link":"#generate-html-reports-from-test-plan-execution","children":[]},{"level":3,"title":"Live built-in graphs and stats","slug":"live-built-in-graphs-and-stats","link":"#live-built-in-graphs-and-stats","children":[]}]},{"level":2,"title":"Response processing","slug":"response-processing","link":"#response-processing","children":[{"level":3,"title":"Check for expected response","slug":"check-for-expected-response","link":"#check-for-expected-response","children":[]},{"level":3,"title":"Check for expected JSON","slug":"check-for-expected-json","link":"#check-for-expected-json","children":[]},{"level":3,"title":"Change sample result statuses with custom logic","slug":"change-sample-result-statuses-with-custom-logic","link":"#change-sample-result-statuses-with-custom-logic","children":[{"level":4,"title":"Lambdas","slug":"lambdas","link":"#lambdas","children":[]}]},{"level":3,"title":"Use part of a response in a subsequent request (aka correlation)","slug":"use-part-of-a-response-in-a-subsequent-request-aka-correlation","link":"#use-part-of-a-response-in-a-subsequent-request-aka-correlation","children":[{"level":4,"title":"Regular expressions extraction","slug":"regular-expressions-extraction","link":"#regular-expressions-extraction","children":[]},{"level":4,"title":"Boundaries based extraction","slug":"boundaries-based-extraction","link":"#boundaries-based-extraction","children":[]},{"level":4,"title":"JSON extraction","slug":"json-extraction","link":"#json-extraction","children":[]}]}]},{"level":2,"title":"Requests generation","slug":"requests-generation","link":"#requests-generation","children":[{"level":3,"title":"Conditionals","slug":"conditionals","link":"#conditionals","children":[]},{"level":3,"title":"Loops","slug":"loops","link":"#loops","children":[{"level":4,"title":"Iterating over extracted values","slug":"iterating-over-extracted-values","link":"#iterating-over-extracted-values","children":[]},{"level":4,"title":"Iterating while a condition is met","slug":"iterating-while-a-condition-is-met","link":"#iterating-while-a-condition-is-met","children":[]},{"level":4,"title":"Iterating a fixed number of times","slug":"iterating-a-fixed-number-of-times","link":"#iterating-a-fixed-number-of-times","children":[]},{"level":4,"title":"Iterating for a given period","slug":"iterating-for-a-given-period","link":"#iterating-for-a-given-period","children":[]},{"level":4,"title":"Execute only once in thread","slug":"execute-only-once-in-thread","link":"#execute-only-once-in-thread","children":[]}]},{"level":3,"title":"Group requests","slug":"group-requests","link":"#group-requests","children":[]},{"level":3,"title":"CSV as input data for requests","slug":"csv-as-input-data-for-requests","link":"#csv-as-input-data-for-requests","children":[]},{"level":3,"title":"Counter","slug":"counter","link":"#counter","children":[]},{"level":3,"title":"Provide request parameters programmatically per request","slug":"provide-request-parameters-programmatically-per-request","link":"#provide-request-parameters-programmatically-per-request","children":[]},{"level":3,"title":"Timers","slug":"timers","link":"#timers","children":[{"level":4,"title":"Emulate user delays between requests","slug":"emulate-user-delays-between-requests","link":"#emulate-user-delays-between-requests","children":[]},{"level":4,"title":"Control throughput","slug":"control-throughput","link":"#control-throughput","children":[]},{"level":4,"title":"Requests synchronization","slug":"requests-synchronization","link":"#requests-synchronization","children":[]}]},{"level":3,"title":"Execute part of a test plan part a fraction of the times","slug":"execute-part-of-a-test-plan-part-a-fraction-of-the-times","link":"#execute-part-of-a-test-plan-part-a-fraction-of-the-times","children":[]},{"level":3,"title":"Switch between test plan parts with a given probability","slug":"switch-between-test-plan-parts-with-a-given-probability","link":"#switch-between-test-plan-parts-with-a-given-probability","children":[]},{"level":3,"title":"Parallel requests","slug":"parallel-requests","link":"#parallel-requests","children":[]}]},{"level":2,"title":"JMeter variables & properties","slug":"jmeter-variables-properties","link":"#jmeter-variables-properties","children":[{"level":3,"title":"Variables","slug":"variables","link":"#variables","children":[]},{"level":3,"title":"Properties","slug":"properties","link":"#properties","children":[]}]},{"level":2,"title":"Test resources","slug":"test-resources","link":"#test-resources","children":[]},{"level":2,"title":"Protocols","slug":"protocols","link":"#protocols","children":[{"level":3,"title":"HTTP","slug":"http","link":"#http","children":[{"level":4,"title":"Methods & body","slug":"methods-body","link":"#methods-body","children":[]},{"level":4,"title":"Parameters","slug":"parameters","link":"#parameters","children":[]},{"level":4,"title":"Headers","slug":"headers","link":"#headers","children":[]},{"level":4,"title":"Authentication","slug":"authentication","link":"#authentication","children":[]},{"level":4,"title":"Multipart requests","slug":"multipart-requests","link":"#multipart-requests","children":[]},{"level":4,"title":"Cookies & caching","slug":"cookies-caching","link":"#cookies-caching","children":[]},{"level":4,"title":"Timeouts","slug":"timeouts","link":"#timeouts","children":[]},{"level":4,"title":"Connections","slug":"connections","link":"#connections","children":[]},{"level":4,"title":"Embedded resources","slug":"embedded-resources","link":"#embedded-resources","children":[]},{"level":4,"title":"Redirects","slug":"redirects","link":"#redirects","children":[]},{"level":4,"title":"HTTP defaults","slug":"http-defaults","link":"#http-defaults","children":[]},{"level":4,"title":"Overriding URL protocol, host or port","slug":"overriding-url-protocol-host-or-port","link":"#overriding-url-protocol-host-or-port","children":[]},{"level":4,"title":"Proxy","slug":"proxy","link":"#proxy","children":[]}]},{"level":3,"title":"GraphQL","slug":"graphql","link":"#graphql","children":[]},{"level":3,"title":"JDBC and databases interactions","slug":"jdbc-and-databases-interactions","link":"#jdbc-and-databases-interactions","children":[]},{"level":3,"title":"Java API performance testing","slug":"java-api-performance-testing","link":"#java-api-performance-testing","children":[]},{"level":3,"title":"Selenium","slug":"selenium","link":"#selenium","children":[]}]},{"level":2,"title":"Custom or yet not supported test elements","slug":"custom-or-yet-not-supported-test-elements","link":"#custom-or-yet-not-supported-test-elements","children":[]},{"level":2,"title":"JMX support","slug":"jmx-support","link":"#jmx-support","children":[{"level":3,"title":"Save as JMX","slug":"save-as-jmx","link":"#save-as-jmx","children":[]},{"level":3,"title":"Run JMX file","slug":"run-jmx-file","link":"#run-jmx-file","children":[]}]}],"git":{},"filePathRelative":"guide/index.md"}');export{e as data}; diff --git a/assets/index.html-60f5385c.js b/assets/index.html-60f5385c.js new file mode 100644 index 00000000..9674f9b8 --- /dev/null +++ b/assets/index.html-60f5385c.js @@ -0,0 +1 @@ +import{_ as o,r as a,o as i,c as n,a as e,b as t,d as r,e as c}from"./app-863e42f0.js";const p="/jmeter-java-dsl/assets/abstracta-logo-63bce99b.png",l="/jmeter-java-dsl/assets/blazemeter-logo-a5731ee5.png",d="/jmeter-java-dsl/assets/octoperf-logo-dc518d38.png",u="/jmeter-java-dsl/assets/azure-logo-1bad44c3.png",h={},m=e("h1",{id:"support",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#support","aria-hidden":"true"},"#"),t(" Support")],-1),g=e("h2",{id:"community-support",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#community-support","aria-hidden":"true"},"#"),t(" Community Support")],-1),f=e("p",null,"The JMeter DSL project has a vibrant and active community that provides extensive support, on a best effort basis, to its users. Community support is primarily offered through the following channels:",-1),_={href:"https://discord.gg/WNSn5hqmSd",target:"_blank",rel:"noopener noreferrer"},b={href:"https://discord.gg/WNSn5hqmSd",target:"_blank",rel:"noopener noreferrer"},v={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},y={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},S={href:"https://github.com/abstracta/jmeter-java-dsl/discussions",target:"_blank",rel:"noopener noreferrer"},w={href:"https://github.com/abstracta/jmeter-java-dsl/discussions",target:"_blank",rel:"noopener noreferrer"},x=e("p",null,"The community is actively involved in proposing new improvements, answering questions, assisting in design decisions, and submitting pull requests. Together, we strive to enhance the capabilities and usability of JMeter DSL.",-1),j=e("h2",{id:"enterprise-support-by-abstracta",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#enterprise-support-by-abstracta","aria-hidden":"true"},"#"),t(" Enterprise Support by Abstracta")],-1),k={href:"https://abstracta.us",target:"_blank",rel:"noopener noreferrer"},D=e("ul",null,[e("li",null,"Dedicated support team : Get prompt answers and peace of mind from a dedicated support team with the expertise to help you resolve issues faster."),e("li",null,"Customizations: Receive tailored solutions to meet your specific requirements."),e("li",null,"Consulting services: Access a team of experts to fine-tune your JMeter DSL usage, speed up implementation, work on your performance testing strategy and overall testing processes.")],-1),L=e("p",null,"Abstracta is committed to helping organizations succeed with JMeter DSL by providing comprehensive support and specialized services tailored to your enterprise needs.",-1),J={href:"https://abstracta.us/contact-us",target:"_blank",rel:"noopener noreferrer"},M=c('

Industry Support

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:

',4);function z(A,I){const s=a("ExternalLinkIcon");return i(),n("div",null,[m,g,f,e("ul",null,[e("li",null,[e("a",_,[t("Discord server"),r(s)]),t(": Join our "),e("a",b,[t("Discord server"),r(s)]),t(" to engage with fellow JMeter DSL enthusiasts. It's a real-time platform where you can ask questions, share experiences, and participate in discussions.")]),e("li",null,[e("a",v,[t("GitHub Issues"),r(s)]),t(": For bug reports, feature requests, or any specific problems you encounter while using JMeter DSL, "),e("a",y,[t("GitHub Issues"),r(s)]),t(" is the place to go. Create an issue, and the community will jump in to assist you, propose improvements, and collaborate on finding solutions.")]),e("li",null,[e("a",S,[t("GitHub Discussions"),r(s)]),t(": If you have open-ended discussions, ideas, or suggestions related to JMeter DSL, head over to "),e("a",w,[t("GitHub Discussions"),r(s)]),t(". It's an excellent platform for brainstorming, gathering feedback, and engaging in community-driven conversations.")])]),x,j,e("p",null,[t("In addition to community support, "),e("a",k,[t("Abstracta"),r(s)]),t(" offers enterprise-level support for JMeter DSL users. Abstracta is the main supporter of JMeter DSL development and provides specialized professional services to ensure the success of organizations using JMeter DSL. With Abstracta's enterprise support, you can accelerate your JMeter DSL implementation and have access to:")]),D,L,e("p",null,[t("To explore Abstracta's enterprise support options or discuss your specific needs, please "),e("a",J,[t("contact the Abstracta team"),r(s)]),t(".")]),M])}const C=o(h,[["render",z],["__file","index.html.vue"]]);export{C as default}; diff --git a/assets/index.html-8c2b6955.js b/assets/index.html-8c2b6955.js new file mode 100644 index 00000000..ab8b7ca1 --- /dev/null +++ b/assets/index.html-8c2b6955.js @@ -0,0 +1,2317 @@ +import{_ as l,r as i,o as u,c as r,a as n,b as s,d as a,w as p,e}from"./app-863e42f0.js";const k="/jmeter-java-dsl/assets/jmdsl-recorder-7b4503f7.gif",d="/jmeter-java-dsl/assets/config-ide-autocomplete-230365f9.png",m="/jmeter-java-dsl/assets/blazemeter-85d7b816.png",v="/jmeter-java-dsl/assets/octoperf-cf8e523e.png",h="/jmeter-java-dsl/assets/azure-35125246.png",b="/jmeter-java-dsl/assets/ultimate-thread-group-timeline-471befb4.png",g="/jmeter-java-dsl/assets/ultimate-thread-group-gui-400bcb90.png",f="/jmeter-java-dsl/assets/rps-thread-group-timeline-11c83237.png",y="/jmeter-java-dsl/assets/view-results-tree-431a3001.png",w="/jmeter-java-dsl/assets/post-processor-debugging-2735516e.png",j="/jmeter-java-dsl/assets/jmeter-http-sampler-debugging-4b2e79d2.png",_="/jmeter-java-dsl/assets/test-plan-gui-a4e8b653.png",q="/jmeter-java-dsl/assets/grafana-ee0c26ae.png",T="/jmeter-java-dsl/assets/datadog-3aaf2aae.png",x="/jmeter-java-dsl/assets/dashboard-777064cf.png",P="/jmeter-java-dsl/assets/not-synchronized-samples-483d50c1.png",S="/jmeter-java-dsl/assets/synchronized-samples-3256a15e.png",I={},D=n("h1",{id:"user-guide",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#user-guide","aria-hidden":"true"},"#"),s(" User guide")],-1),E=n("p",null,"Here we share some tips and examples on how to use the DSL to tackle common use cases.",-1),J={href:"https://junit.org/junit5/",target:"_blank",rel:"noopener noreferrer"},C={href:"https://joel-costigliola.github.io/assertj/assertj-core-quick-start.html",target:"_blank",rel:"noopener noreferrer"},A={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl",target:"_blank",rel:"noopener noreferrer"},M={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},O={class:"custom-container tip"},L=n("p",{class:"custom-container-title"},"TIP",-1),R={href:"https://github.com/abstracta/jmeter-java-dsl",target:"_blank",rel:"noopener noreferrer"},G={href:"http://jmeter.apache.org/usermanual/get-started.html",target:"_blank",rel:"noopener noreferrer"},N=n("h2",{id:"setup",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#setup","aria-hidden":"true"},"#"),s(" Setup")],-1),z=n("p",null,"To use the DSL just include it in your project:",-1),U=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),B=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[n("span",{class:"token function"},"testImplementation"),n("span",{class:"token punctuation"},"("),n("span",{class:"token interpolation-string"},[n("span",{class:"token string"},'"us.abstracta.jmeter:jmeter-java-dsl:1.29"')]),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token function"},"exclude"),n("span",{class:"token punctuation"},"("),n("span",{class:"token interpolation-string"},[n("span",{class:"token string"},'"org.apache.jmeter"')]),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token interpolation-string"},[n("span",{class:"token string"},'"bom"')]),n("span",{class:"token punctuation"},")"),s(` +`),n("span",{class:"token punctuation"},"}"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),H={class:"custom-container tip"},W=n("p",{class:"custom-container-title"},"TIP",-1),F={href:"https://github.com/abstracta/jmeter-java-dsl-sample",target:"_blank",rel:"noopener noreferrer"},V=e(`

Simple HTTP test plan

To generate HTTP requests just use provided httpSampler.

The following example uses 2 threads (concurrent users) that send 10 HTTP GET requests each to http://my.service.

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.

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        //this is just to log details of each request stats
+        jtlWriter("target/jtls")
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

TIP

When working with multiple samplers in a test plan, specify their names (eg: httpSampler("home", "http://my.service")) to easily check their respective statistics.

TIP

Set connection and response timeouts to avoid potential execution differences when running test plan in different machines. Here are more details.

`,7),Y={class:"custom-container tip"},$=n("p",{class:"custom-container-title"},"TIP",-1),X={href:"https://logging.apache.org/log4j/2.x/",target:"_blank",rel:"noopener noreferrer"},K={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/test/resources/log4j2.xml",target:"_blank",rel:"noopener noreferrer"},Q={class:"custom-container tip"},Z=n("p",{class:"custom-container-title"},"TIP",-1),nn={href:"https://github.com/abstracta/jmeter-java-dsl/issues/26#issuecomment-953783407",target:"_blank",rel:"noopener noreferrer"},sn=n("p",null,[s("Check "),n("a",{href:"#http"},"HTTP performance testing"),s(" for additional details while testing HTTP services.")],-1),an=n("h2",{id:"dsl-recorder",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#dsl-recorder","aria-hidden":"true"},"#"),s(" DSL recorder")],-1),tn=n("p",null,"When creating test plans you can rely just on the IDE or you can use provided recorder.",-1),en=n("p",null,"Here is a small demo using it:",-1),pn=n("p",null,[n("img",{src:k,alt:"jmdsl recorder demo"})],-1),on={class:"custom-container tip"},cn=n("p",{class:"custom-container-title"},"TIP",-1),ln={href:"https://www.jbang.dev/documentation/guide/latest/index.html",target:"_blank",rel:"noopener noreferrer"},un=n("div",{class:"language-bash line-numbers-mode","data-ext":"sh"},[n("pre",{class:"language-bash"},[n("code",null,`jbang us.abstracta.jmeter:jmeter-java-dsl-cli:1.29 recorder http://retailstore.test +`)]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),rn=e('

TIP

Use java -jar jmdsl.jar help recorder to see the list of options to customize your recording.

TIP

In general use ---url-includes to ignore URLs that are not relevant to the performance test.

WARNING

Unlike the rest of JMeter DSL, which is compiled with Java 8, jmdsl.jar and us.abstracta.jmeter:jmeter-java-dsl-cli are compiled with Java 11 due to some dependencies requirement (latest Selenium drivers mainly).

So, to run above commands, you will need Java 11 or newer.

Correlations

',4),kn={href:"https://github.com/Blazemeter/CorrelationRecorder",target:"_blank",rel:"noopener noreferrer"},dn=e(`

Correlation rules define regular expressions, which allow the recorder to automatically add regexExtractor and replace occurrences of extracted values in following requests with proper variable references.

For example, for the same scenario previously shown, and using --config option (which makes correlation rules easier to maintain) with following file:

recorder:
+  url: http://retailstore.test
+  urlIncludes:
+    - retailstore.test.*
+  correlations:
+    - variable: productId
+      extractor: name="productId" value="([^"]+)"
+      replacement: productId=(.*)
+

We get this test plan:

///usr/bin/env jbang "$0" "$@" ; exit $?
+/*
+These commented lines make the class executable if you have jbang installed by making the file
+executable (eg: chmod +x ./PerformanceTest.java) and just executing it with ./PerformanceTest.java
+*/
+//DEPS org.assertj:assertj-core:3.23.1
+//DEPS org.junit.jupiter:junit-jupiter-engine:5.9.1
+//DEPS org.junit.platform:junit-platform-launcher:1.9.1
+//DEPS us.abstracta.jmeter:jmeter-java-dsl:1.29
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import org.apache.http.entity.ContentType;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void test() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(1, 1,
+          httpDefaults()
+            .encoding(StandardCharsets.UTF_8),
+          httpSampler("/-1", "http://retailstore.test"),
+          httpSampler("/home-3", "http://retailstore.test/home")
+            .children(
+              regexExtractor("productId#2", "name=\\"productId\\" value=\\"([^\\"]+)\\"")
+                .defaultValue("productId#2_NOT_FOUND")
+            ),
+          httpSampler("/cart-16", "http://retailstore.test/cart")
+            .method(HTTPConstants.POST)
+            .contentType(ContentType.APPLICATION_FORM_URLENCODED)
+            .rawParam("productId", "\${productId#2}"),
+          httpSampler("/cart-17", "http://retailstore.test/cart")
+        )
+    ).run();
+    assertThat(stats.overall().errorsCount()).isEqualTo(0);
+  }
+
+  /*
+   This method is only included to make the test class self-executable. You can remove it when
+   executing tests with maven, gradle, or some other tool.
+   */
+  public static void main(String[] args) {
+    SummaryGeneratingListener summaryListener = new SummaryGeneratingListener();
+    LauncherFactory.create()
+        .execute(LauncherDiscoveryRequestBuilder.request()
+                .selectors(DiscoverySelectors.selectClass(PerformanceTest.class))
+                .build(),
+            summaryListener);
+    TestExecutionSummary summary = summaryListener.getSummary();
+    summary.printFailuresTo(new PrintWriter(System.err));
+    System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0);
+  }
+
+}
+

In this test plan you can see an already added an extractor and the usage of extracted value in a subsequent request (as a variable reference).

`,6),mn={class:"custom-container tip"},vn=n("p",{class:"custom-container-title"},"TIP",-1),hn=n("p",null,[s("To identify potential correlations, you can check in request parameters or URLs with fixed values and then, check the automatically created recording "),n("code",null,".jtl"),s(" file (by default in "),n("code",null,"target/recording"),s(" folder) to identify proper regular expression for extraction.")],-1),bn={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},gn=e('

TIP

When using --config, take advantage of your IDEs auto-completion and inline documentation capabilities by using .jmdsl.yml suffix in config file names.

Here is a screenshot of autocompletion in action:

Config file IDE autocomplete

DSL code generation from JMX file

',2),fn=n("code",null,"jmx2dsl",-1),yn={href:"https://github.com/abstracta/jmeter-java-dsl/releases",target:"_blank",rel:"noopener noreferrer"},wn={href:"https://www.jbang.dev/documentation/guide/latest/index.html",target:"_blank",rel:"noopener noreferrer"},jn=n("p",null,"As an example:",-1),_n=n("div",{class:"language-bash line-numbers-mode","data-ext":"sh"},[n("pre",{class:"language-bash"},[n("code",null,[n("span",{class:"token function"},"java"),s(),n("span",{class:"token parameter variable"},"-jar"),s(` jmdsl.jar jmx2dsl test-plan.jmx +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),qn=n("div",{class:"language-bash line-numbers-mode","data-ext":"sh"},[n("pre",{class:"language-bash"},[n("code",null,`jbang us.abstracta.jmeter:jmeter-java-dsl-cli:1.29 jmx2dsl test-plan.jmx +`)]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),Tn=e(`

Could generate something like the following output:

///usr/bin/env jbang "$0" "$@" ; exit $?
+/*
+These commented lines make the class executable if you have jbang installed by making the file
+executable (eg: chmod +x ./PerformanceTest.java) and just executing it with ./PerformanceTest.java
+*/
+//DEPS org.assertj:assertj-core:3.23.1
+//DEPS org.junit.jupiter:junit-jupiter-engine:5.9.1
+//DEPS org.junit.platform:junit-platform-launcher:1.9.1
+//DEPS us.abstracta.jmeter:jmeter-java-dsl:1.29
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void test() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        jtlWriter("target/jtls")
+    ).run();
+    assertThat(stats.overall().errorsCount()).isEqualTo(0);
+  }
+
+  /*
+   This method is only included to make the test class self-executable. You can remove it when
+   executing tests with maven, gradle, or some other tool.
+   */
+  public static void main(String[] args) {
+    SummaryGeneratingListener summaryListener = new SummaryGeneratingListener();
+    LauncherFactory.create()
+        .execute(LauncherDiscoveryRequestBuilder.request()
+                .selectors(DiscoverySelectors.selectClass(PerformanceTest.class))
+                .build(),
+            summaryListener);
+    TestExecutionSummary summary = summaryListener.getSummary();
+    summary.printFailuresTo(new PrintWriter(System.err));
+    System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0);
+  }
+
+}
+

WARNING

Unlike the rest of JMeter DSL which is compiled with Java 8, jmdsl.jar and us.abstracta.jmeter:jmeter-java-dsl-cli are compiled with Java 11 due to some dependencies requirement (latest Selenium drivers mainly).

So, to run above commands, you will need Java 11 or newer.

TIP

Review and try generated code before executing it as is. I.e: tune thread groups and iterations to 1 to give it a try.

TIP

Always review generated DSL code. You should add proper assertions to it, might want to clean it up, add to your maven or gradle project dependencies listed on initial comments of generated code, modularize it better, check that conversion is accurate according to DSL, or even propose improvements for it in the GitHub repository.

`,5),xn={class:"custom-container tip"},Pn=n("p",{class:"custom-container-title"},"TIP",-1),Sn=n("p",null,"Conversions can always be improved, and since there are many combinations and particular use cases, different semantics, etc, getting a perfect conversion for every scenario can get tricky.",-1),In={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},Dn={href:"https://github.com/abstracta/jmeter-java-dsl/discussions",target:"_blank",rel:"noopener noreferrer"},En=n("h2",{id:"run-test-at-scale",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#run-test-at-scale","aria-hidden":"true"},"#"),s(" Run test at scale")],-1),Jn=n("p",null,"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.",-1),Cn=n("h3",{id:"blazemeter",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#blazemeter","aria-hidden":"true"},"#"),s(" BlazeMeter")],-1),An=n("p",null,"By including the following module as a dependency:",-1),Mn=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-blazemeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),On=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-blazemeter:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),Ln={href:"https://www.blazemeter.com/",target:"_blank",rel:"noopener noreferrer"},Rn=e(`
import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.blazemeter.BlazeMeterEngine;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws Exception {
+    TestPlanStats 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(System.getenv("BZ_TOKEN"))
+        .testName("DSL test")
+        .totalUsers(500)
+        .holdFor(Duration.ofMinutes(10))
+        .threadsPerEngine(100)
+        .testTimeout(Duration.ofMinutes(20)));
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

This test is using BZ_TOKEN, a custom environment variable with <KEY_ID>:<KEY_SECRET> format, to get the BlazeMeter API authentication credentials.

`,2),Gn={href:"https://guide.blazemeter.com/hc/en-us/articles/115002213289-BlazeMeter-API-keys-",target:"_blank",rel:"noopener noreferrer"},Nn=n("code",null,".runIn(new BlazeMeterEngine(...))",-1),zn=n("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),Un=n("p",null,[n("img",{src:m,alt:"BlazeMeter Example Execution Dashboard"})],-1),Bn={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-blazemeter/src/main/java/us/abstracta/jmeter/javadsl/blazemeter/BlazeMeterEngine.java",target:"_blank",rel:"noopener noreferrer"},Hn=e(`

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

BlazeMeterEngine always returns 0 as sentBytes statistics since there is no efficient way to get it from BlazMeter.

TIP

BlazeMeterEngine will automatically upload to BlazeMeter files used in csvDataSet and httpSampler with bodyFile or bodyFilePart methods.

For example this test plan works out of the box (no need for uploading referenced files or adapt test plan):

testPlan(
+    threadGroup(100, Duration.ofMinutes(5),
+      csvDataSet(new TestResource("users.csv")),
+      httpSampler(SAMPLE_LABEL, "https://myservice/users/\${USER}")
+    )
+).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN"))
+    .testTimeout(Duration.ofMinutes(10)));
+

If you need additional files to be uploaded to BlazeMeter, you can easily specify them with the BlazemeterEngine.assets() method.

TIP

By default BlazeMeterEngine will run tests from default location (most of the times us-east4-a). But in some scenarios you might want to change the location, or even run the test from multiple locations.

Here is an example how you can easily set this up:

testPlan(
+    threadGroup(300, Duration.ofMinutes(5), // 300 total users for 5 minutes
+      httpSampler(SAMPLE_LABEL, "https://myservice")
+    )
+).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN"))
+    .location(BlazeMeterLocation.GCP_SAO_PAULO, 30) // 30% = 90 users will run in Google Cloud Platform at Sao Paulo
+    .location("MyPrivateLocation", 70) // 70% = 210 users will run in MyPrivateLocation named private location
+    .testTimeout(Duration.ofMinutes(10)));
+

TIP

In case you want to get debug logs for HTTP calls to BlazeMeter API, you can include the following setting to an existing log4j2.xml configuration file:

<Logger name="us.abstracta.jmeter.javadsl.blazemeter.BlazeMeterClient" level="DEBUG"/>
+<Logger name="okhttp3" level="DEBUG"/>
+

WARNING

If you use test elements (JSR223 elements, httpSamplers, ifController or whileController) with Java lambdas instead of strings, check this section of the user guide to use them while running test plan in BlazeMeter.

OctoPerf

In the same fashion as with BlazeMeter, just by including the following module as a dependency:

`,8),Wn=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-octoperf"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),Fn=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-octoperf:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),Vn={href:"https://octoperf.com/",target:"_blank",rel:"noopener noreferrer"},Yn=e(`
import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.octoperf.OctoPerfEngine;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws Exception {
+    TestPlanStats stats = testPlan(
+        // number of threads and iterations are in the end overwritten by OctoPerf engine settings 
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        )
+    ).runIn(new OctoPerfEngine(System.getenv("OCTOPERF_API_KEY"))
+        .projectName("DSL test")
+        .totalUsers(500)
+        .rampUpFor(Duration.ofMinutes(1))
+        .holdFor(Duration.ofMinutes(10))
+        .testTimeout(Duration.ofMinutes(20)));
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

This test is using OCTOPERF_API_KEY, a custom environment variable containing an OctoPerf API key.

`,2),$n={href:"https://doc.octoperf.com/account/profile/#apikey",target:"_blank",rel:"noopener noreferrer"},Xn=n("code",null,".runIn(new OctoPerfEngine(...))",-1),Kn=n("p",null,"As with the BlazeMeter case, with OctoPerf 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 OctoPerf:",-1),Qn=n("p",null,[n("img",{src:v,alt:"OctoPerf Example Execution Dashboard"})],-1),Zn={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-octoperf/src/main/java/us/abstracta/jmeter/javadsl/octoperf/OctoPerfEngine.java",target:"_blank",rel:"noopener noreferrer"},ns=e(`

WARNING

To avoid piling up virtual users and scenarios in OctoPerf project, OctoPerfEngine deletes any OctoPerfEngine previously created entities (virtual users and scenarios with jmeter-java-dsl tag) in the project.

It is very important that you use different project names for different projects to avoid interference (parallel execution of two jmeter-java-dsl projects).

If you want to disable this automatic cleanup, you can use the existing OctoPerfEngine method .projectCleanUp(false).

TIP

In case you want to get debug logs for HTTP calls to OctoPerf API, you can include the following setting to an existing log4j2.xml configuration file:

<Logger name="us.abstracta.jmeter.javadsl.octoperf.OctoPerfClient" level="DEBUG"/>
+<Logger name="okhttp3" level="DEBUG"/>
+
`,2),ss={class:"custom-container warning"},as=n("p",{class:"custom-container-title"},"WARNING",-1),ts=n("code",null,"OctoPerfEngine",-1),es=n("code",null,"BlazeMeterEngine",-1),ps={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},os=n("div",{class:"custom-container warning"},[n("p",{class:"custom-container-title"},"WARNING"),n("p",null,[s("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 OctoPerf execution not detected by the client, and avoid keeping the test indefinitely running until is interrupted by a user, which is specially annoying when running tests in automated fashion, for example in CI/CD. It is strongly advised to "),n("strong",null,"set this timeout properly in each run"),s(", according to the expected test execution time plus some additional margin (to consider for additional delays in OctoPerf test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or unnecessary waits when there is some unexpected issue with OctoPerf execution.")])],-1),cs=n("h3",{id:"azure-load-testing",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#azure-load-testing","aria-hidden":"true"},"#"),s(" Azure Load Testing")],-1),is={href:"https://azure.microsoft.com/en-us/products/load-testing/",target:"_blank",rel:"noopener noreferrer"},ls=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-azure"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),us=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-azure:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),rs=e(`

And using the provided engine like this:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.azure.AzureEngine;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws Exception {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        )
+    ).runIn(new AzureEngine(System.getenv("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(Duration.ofMinutes(20)));
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,2),ks=n("code",null,"AZURE_CREDS",-1),ds=n("code",null,"tenantId:clientId:clientSecret",-1),ms={href:"https://portal.azure.com/#view/Microsoft_AAD_IAM/TenantPropertiesBlade",target:"_blank",rel:"noopener noreferrer"},vs={href:"https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal",target:"_blank",rel:"noopener noreferrer"},hs=n("p",null,"As with the BlazeMeter and OctoPerf, 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:",-1),bs=n("p",null,[n("img",{src:h,alt:"Azure Load Testing Example Execution Dashboard"})],-1),gs={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-azure/src/main/java/us/abstracta/jmeter/javadsl/azure/AzureEngine.java",target:"_blank",rel:"noopener noreferrer"},fs=e(`

TIP

AzureEngine will automatically upload to Azure Load Testing files used in csvDataSet and httpSampler with bodyFile or bodyFilePart methods.

For example this test plan works out of the box (no need for uploading referenced files or adapt test plan):

testPlan(
+    threadGroup(100, Duration.ofMinutes(5),
+      csvDataSet(new TestResource("users.csv")),
+      httpSampler(SAMPLE_LABEL, "https://myservice/users/\${USER}")
+    )
+).runIn(new AzureEngine(System.getenv("BZ_TOKEN"))
+    .testTimeout(Duration.ofMinutes(10)));
+

If you need additional files to be uploaded to Azure Load Testing, you can easily specify them with the AzureEngine.assets() method.

TIP

If you use a csvDataSet and multiple Azure engines (through the engines() method) and want to split provided CSVs between the Azure engines, as to not generate same requests from each engine, then you can use splitCsvsBetweenEngines.

TIP

If you want to correlate test runs to other entities (like a CI/CD job id, product version release, git commit, etc) you can add such information in the test run name by using the testRunName() method.

TIP

To get a full view in Azure Load Testing test run execution report not only of the performance test collected metrics, but also metrics from the application components under test, you can register all the application components using the monitoredResources() method.

monitoredResources() requires a list of resources ids, which you can get by navigating in Azure portal to the correct resource, and then copy part of the url from the browser. For example, a resource id for a container app looks like /subscriptions/my-subscription-id/resourceGroups/my-resource-group/providers/Microsoft.App/containerapps/my-papp.

TIP

As with BlazeMeter and OctoPerf cases, if you want to get debug logs for HTTP calls to Azure API, you can include the following setting to an existing log4j2.xml configuration file:

<Logger name="us.abstracta.jmeter.javadsl.azure.AzureClient" level="DEBUG"/>
+<Logger name="okhttp3" level="DEBUG"/>
+
`,5),ys={class:"custom-container warning"},ws=n("p",{class:"custom-container-title"},"WARNING",-1),js=n("code",null,"AzureEngine",-1),_s=n("code",null,"BlazeMeterEngine",-1),qs={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},Ts=n("div",{class:"custom-container warning"},[n("p",{class:"custom-container-title"},"WARNING"),n("p",null,[s("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 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 in unnecessary expenses in Azure and is specially annoying when running tests in automated fashion, for example in CI/CD. It is strongly advised to "),n("strong",null,"set this timeout properly in each run"),s(", 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.")])],-1),xs=n("h3",{id:"jmeter-remote-testing",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#jmeter-remote-testing","aria-hidden":"true"},"#"),s(" JMeter remote testing")],-1),Ps={href:"http://jmeter.apache.org/usermanual/remote-test.html",target:"_blank",rel:"noopener noreferrer"},Ss=e(`

JMeter remote testing requires setting up nodes in server/slave mode (using bin/jmeter-server JMeter script) with a configured keystore (usually rmi_keystore.jks, generated with bin/ JMeter script) which will execute a test plan triggered in a client/master node.

You can trigger such tests with the DSL using DistributedJmeterEngine as in the following example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.engines.DistributedJmeterEngine;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws Exception {
+    TestPlanStats stats = testPlan(
+        threadGroup(200, Duration.ofMinutes(10),
+            httpSampler("http://my.service")
+        )
+    ).runIn(new DistributedJmeterEngine("host1", "host2"));
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

This will run 200 users for 10 minutes on each server/slave (host1 and host2) and aggregate all the results in returned stats.

`,4),Is={class:"custom-container warning"},Ds=e(`

WARNING

Use same version used by JMeter DSL when setting up the cluster to avoid any potential issues.

For instance, JMeter 5.6 has introduced some changes that currently break some plugins using by JMeter DSL, or change default behavior for test plans.

To find out the current version of JMeter DSL you can check JMeter jars version in your project dependency tree. E.g.:

mvn dependency:tree -Dincludes=org.apache.jmeter:ApacheJMeter_core
+
`,5),Es={href:"https://github.com/abstracta/jmeter-java-dsl/blob/8a8eb303d1013676bf669116ddc8f056e8a445d8/pom.xml#L51",target:"_blank",rel:"noopener noreferrer"},Js=n("code",null,"jmeter.version",-1),Cs=n("div",{class:"custom-container warning"},[n("p",{class:"custom-container-title"},"WARNING"),n("p",null,[s("To be able to run the test you require the "),n("code",null,"rmi_keystore.jks"),s(" file in the working directory of the test. For the time being, we couldn't find a way to allow setting any arbitrary path for the file.")])],-1),As=n("div",{class:"custom-container warning"},[n("p",{class:"custom-container-title"},"WARNING"),n("p",null,"In general, prefer using BlazeMeter, OctoPerf or Azure options which avoid all the setup and maintenance costs of the infrastructure required by JMeter remote testing, also benefiting from other additional useful features they provide (like reporting capabilities).")],-1),Ms={class:"custom-container tip"},Os=n("p",{class:"custom-container-title"},"TIP",-1),Ls={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/docs/guide/scale/distributed",target:"_blank",rel:"noopener noreferrer"},Rs=n("code",null,"docker-compose",-1),Gs={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/engines/DistributedJmeterEngine.java",target:"_blank",rel:"noopener noreferrer"},Ns={href:"http://jmeter.apache.org/usermanual/remote-test.html",target:"_blank",rel:"noopener noreferrer"},zs=e(`

Auto Stop

As previously shown, it is quite easy to check after test plan execution if the collected metrics are the expected ones and fail/pass the test accordingly.

But, what if you want to stop your test plan as soon as the metrics deviate from expected ones? This could help avoiding unnecessary resource usage, especially when conducting tests at scale to avoid incurring additional costs.

With JMeter DSL you can easily define auto-stop conditions over collected metrics, that when met will stop the test plan and throw an exception that will make your test fail.

Here is an example:

import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+import static us.abstracta.jmeter.javadsl.core.listeners.AutoStopListener.AutoStopCondition.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, Duration.ofMinutes(1),
+          httpSampler("http://my.service")
+        ),
+        autoStop()
+          .when(errors().total().greaterThan(0)) // when any sample fails, then test plan will stop and an exception will be thrown pointing to this condition.
+    ).run();
+  }
+
+}
+
+
`,6),Us={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/listeners/AutoStopListener.java",target:"_blank",rel:"noopener noreferrer"},Bs=n("code",null,"autoStop",-1),Hs={href:"https://jmeter-plugins.org/wiki/AutoStop/",target:"_blank",rel:"noopener noreferrer"},Ws=e('

TIP

autoStop will only consider samples within its scope.

If you place it as a test plan child, then it will evaluate metrics for all samples. If you place it as a thread group child, then it will evaluate metrics for samples of such thread group. If you place it as a controller child, then only samples within such controller. And, if you place it as a sampler child, it will only evaluate samples for that particular sampler.

Additionally, you can use the samplesMatching(regex) method to only evaluate metrics for a subset of samples within a given scope (eg: all samples with a label starting with users).

TIP

You can add multiple autoStop elements within a test plan. The first one containing a condition that is met will trigger the auto-stop.

To identify which autoStop element triggered, you can specify a name, like autoStop("login"), and the associated name will be included in the exception thrown by autoStop when the test plan is stopped.

Additionally, you can specify several conditions on an autoStop element. When any of such conditions are met, then the test plan is stopped.

',2),Fs={class:"custom-container tip"},Vs=n("p",{class:"custom-container-title"},"TIP",-1),Ys=n("p",null,[s("By default, "),n("code",null,"autoStop"),s(" will evaluate each condition for each sample and stop the test plan as soon as a condition is met.")],-1),$s={href:"https://jmeter-plugins.org/wiki/AutoStop/",target:"_blank",rel:"noopener noreferrer"},Xs=e("

To change this behavior you can use the every(Duration) method (after specifying the aggregation method, eg errors().perSecond().every(Duration.ofSeconds(5)))) to specify that the condition should only be evaluated, and the aggregation reset, for every given period.

This is particularly helpful for some aggregations (like mean, perSecond, and percent) which may get "stuck" due to historical values collected for the metric.

As an example to illustrate this issue, consider the scenario where after 10 minutes you get 10k requests with an average sample time of 1 second, but in the last 10 seconds you get 10 requests with an average of 10 seconds. In this scenario, the general average will not be much affected by the last seconds, but you would in any case want to stop the test plan since last seconds average has been way up the expected value. This is a clear scenario where you would like to use the every() method.

",3),Ks=n("div",{class:"custom-container tip"},[n("p",{class:"custom-container-title"},"TIP"),n("p",null,[s("By default, "),n("code",null,"autoStop"),s(" will stop the test plan as soon as the condition is met, but in many cases it is better to wait for the condition to be met for some period of time, to avoid some intermittent or short-lived condition. To not stop the test plan until the condition holds for a given period of time, you can use "),n("code",null,"holdsFor(Duration)"),s(" at the end of your condition.")])],-1),Qs={class:"custom-container warning"},Zs=n("p",{class:"custom-container-title"},"WARNING",-1),na=n("code",null,"autoStop",-1),sa=n("code",null,"AzureEngine",-1),aa=n("code",null,"BlazeMeterEngine",-1),ta=n("code",null,"OctoPerfEngine",-1),ea={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},pa=e(`

Advanced threads configuration

jmeter-java-dsl provides two simple ways of creating thread groups which are used in most scenarios:

  • specifying threads and the number of iterations each thread should execute before ending the test plan
  • specifying threads and duration for which each thread should execute before the test plan ends

This is how they look in code:

threadGroup(10, 20, ...) // 10 threads for 20 iterations each
+threadGroup(10, Duration.ofSeconds(20), ...) // 10 threads for 20 seconds each
+

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).

Thread ramps and holds

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:

threadGroup().rampTo(10, Duration.ofSeconds(5)).holdIterating(20) // ramp to 10 threads for 5 seconds (1 thread every half second) and iterating each thread 20 times
+threadGroup().rampToAndHold(10, Duration.ofSeconds(5), Duration.ofSeconds(20)) //similar as above but after ramping up holding execution for 20 seconds
+

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:

threadGroup()
+    .rampToAndHold(10, Duration.ofSeconds(5), Duration.ofSeconds(20))
+    .rampToAndHold(100, Duration.ofSeconds(10), Duration.ofSeconds(30))
+    .rampTo(200, Duration.ofSeconds(10))
+    .rampToAndHold(100, Duration.ofSeconds(10), Duration.ofSeconds(30))
+    .rampTo(0, Duration.ofSeconds(5))
+    .children(
+      httpSampler("http://my.service")
+    )
+

Which would translate into the following threads' timeline:

Thread Group Timeline

',14),oa={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/DslDefaultThreadGroup.java",target:"_blank",rel:"noopener noreferrer"},ca=e('

TIP

To visualize the threads timeline, for complex thread group configurations like the previous one, you can get a chart like the previous one by using provided DslThreadGroup.showTimeline() method.

TIP

If you are a JMeter GUI user, you may even be interested in using provided 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).

For example, for the above test plan you would get a window like the following one:

UltimateThreadGroup GUI

TIP

When using multiple thread groups in a test plan, consider setting a name (eg: threadGroup("main", 1, 1, ...)) on them to properly identify associated requests in statistics & jtl results.

Throughput based thread group

Sometimes you want to focus just on the number of requests per second to generate and don't want to be concerned about how many concurrent threads/users, and pauses between requests, are needed. For these scenarios you can use rpsThreadGroup like in the following example:

rpsThreadGroup()
+    .maxThreads(500)
+    .rampTo(20, Duration.ofSeconds(10))
+    .rampTo(10, Duration.ofSeconds(10))
+    .rampToAndHold(1000, Duration.ofSeconds(5), Duration.ofSeconds(10))
+    .children(
+      httpSampler("http://my.service")
+    )
+
`,6),ia={href:"https://jmeter-plugins.org/wiki/ConcurrencyThreadGroup/",target:"_blank",rel:"noopener noreferrer"},la={href:"https://jmeter-plugins.org/wiki/ThroughputShapingTimer/",target:"_blank",rel:"noopener noreferrer"},ua=e('

TIP

rpsThreadGroup will dynamically create and remove threads and add delays between requests to match the traffic to the expected RPS. You can also specify to control iterations per second (the number of times the flow in the thread group runs per second) instead of threads by using .counting(RpsThreadGroup.EventType.ITERATIONS).

WARNING

RPS values control how often to adjust threads and waits. Avoid too low (eg: under 1) values which can cause big waits and don't match the expected RPS.

JMeter Throughput Shaping Timer calculates each time the delay to be used not taking into consideration future expected RPS. For instance, if you configure 1 thread with a ramp from 0.01 to 10 RPS with 10 seconds duration, when 1 request is sent it will calculate that to match 0.01 RPS has to wait requestsCount/expectedRPS = 1/0.01 = 100 seconds, which would keep the thread stuck for 100 seconds when in fact should have done two additional requests after waiting 1 second (to match the ramp). Setting this value greater or equal to 1 will assure at least 1 evaluation every second.

WARNING

When no maxThreads are specified, rpsThreadGroup will use as many threads as needed. In such scenarios, you might face an unexpected number of threads with associated CPU and Memory requirements, which may affect the performance test metrics. You should always set maximum threads to use to avoid such scenarios.

You can use the following formula to calculate a value for maxThreads: T*R, being T the maximum RPS that you want to achieve and R the maximum expected response time (or iteration time if you use .counting(RpsThreadGroup.EventType.ITERATIONS)) in seconds.

TIP

As with the default thread group, with rpsThreadGroup you can use showTimeline to get a chart of configured RPS profile for easy visualization. An example chart:

RPS Thread Group Timeline

',4),ra={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/RpsThreadGroup.java",target:"_blank",rel:"noopener noreferrer"},ka=e(`

Set up & tear down

When you need to run some custom logic before or after a test plan, the simplest approach is just adding plain java code to it, or using your test framework (eg: JUnit) provided features for this purpose. Eg:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @BeforeEach
+  public void setup() {
+    // my custom setup logic
+  }
+
+  @AfterEach
+  public void setup() {
+    // my custom setup logic
+  }
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        )
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

But, in some cases you may need the logic to run inside the JMeter execution context (eg: set some JMeter properties), or, when the test plan runs at scale, to run in the same host where the test plan runs (for example to use some common file).

In such scenarios you can use provided setupThreadGroup & teardownThreadGroup like in the following example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        setupThreadGroup(
+            httpSampler("http://my.service/tokens")
+                .method(HTTPConstants.POST)
+                .children(
+                    jsr223PostProcessor("props.put('MY_TEST_TOKEN', prev.responseDataAsString)")
+                )
+        ),
+        threadGroup(2, 10,
+            httpSampler("http://my.service/products")
+                .header("X-MY-TOKEN", "\${__P(MY_TEST_TOKEN)}")
+        ),
+        teardownThreadGroup(
+            httpSampler("http://my.service/tokens/\${__P(MY_TEST_TOKEN)}")
+                .method(HTTPConstants.DELETE)
+        )
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

TIP

By default, JMeter automatically executes teardown thread groups when a test plan stops due to an unscheduled event like a sample error when a stop test action is configured in a thread group, invocation of ctx.getEngine().askThreadsToStop() in jsr223 element, etc. You can disable this behavior by using the testPlan tearDownOnlyAfterMainThreadsDone method, which might be helpful if the teardown thread group has only to run on clean test plan completion.

`,7),da={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/DslSetupThreadGroup.java",target:"_blank",rel:"noopener noreferrer"},ma={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/DslTeardownThreadGroup.java",target:"_blank",rel:"noopener noreferrer"},va=e(`

Thread groups order

By default, when you add multiple thread groups to a test plan, JMeter will run them all in parallel. This is a very helpful behavior in many cases, but in some others, you may want to run them sequentially (one after the other). To achieve this you can just use sequentialThreadGroups() test plan method.

Test plan debugging

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-java-dsl you have several options for this purpose.

View results tree

One option is using provided resultsTreeVisualizer() like in the following example:

import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    testPlan(
+        threadGroup(1, 1,
+            httpSampler("http://my.service")
+        ),
+        resultsTreeVisualizer()
+    ).run();
+  }
+
+}
+

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:

View Results Tree GUI

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 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.

TIP

Remove 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.

WARNING

By default, View Results Tree only displays the last 500 sample results. If you need to display more elements, use provided 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.

Post-processor breakpoints

Another alternative is using IDE's built-in debugger by adding a jsr223PostProcessor with java code and adding a breakpoint to the post-processor code. This does not only allow checking sample result information but also JMeter variables and properties values and sampler properties.

Here is an example screenshot using this approach while debugging with an IDE:

Post Processor debugging in IDE

',17),ha={class:"custom-container tip"},ba=n("p",{class:"custom-container-title"},"TIP",-1),ga=n("code",null,"varsMap()",-1),fa=n("code",null,"prevMap()",-1),ya=n("code",null,"prevMetadata()",-1),wa=n("code",null,"prevMetrics()",-1),ja=n("code",null,"prevRequest()",-1),_a=n("code",null,"prevResponse()",-1),qa={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/postprocessors/DslJsr223PostProcessor.java",target:"_blank",rel:"noopener noreferrer"},Ta={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/testelements/DslJsr223TestElement.java",target:"_blank",rel:"noopener noreferrer"},xa=e(`

TIP

Remove such post processors when no longer needed (when debugging is finished). Leaving them would generate errors when loading generated JMX test plan or running the test plan in BlazeMeter, OctoPerf or Azure, in addition to unnecessary processing time and resource usage.

Debug info during test plan execution

Another option that allows collecting debugging information during a test plan execution without affecting test plan execution (doesn't stop the test plan on each breakpoint as IDE debugger does, which will affect test plan collected metrics) and allows analyzing information after test plan execution, is using debugPostProcessor which adds a sub result to sampler results including debug information.

Here is an example that collects JMeter variables that can be reviewed with included resultsTreeVisualizer:

import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    String userIdVarName = "USER_ID";
+    String usersPath = "/users";
+    testPlan(
+        httpDefaults().url("http://my.service"),
+        threadGroup(1, 1,
+            httpSampler(usersPath)
+                .children(
+                    jsonExtractor(userIdVarName, "[].id"),
+                    debugPostProcessor()
+                ),
+            httpSampler(usersPath + "/\${" + userIdVarName + "}")
+        ),
+        resultsTreeVisualizer()
+    ).run();
+  }
+
+}
+

This approach is particularly helpful when debugging extractors, allowing you to see what JMeter variables were or were not generated by previous extractors.

In general, prefer using Post processor with IDE debugger breakpoint in the initial stages of test plan development, testing with just 1 thread in thread groups, and using this later approach when trying to debug issues that are reproducible only in multiple threads executions or in a particular environment that requires offline analysis (analyze collected information after test plan execution).

TIP

Use this element in combination with resultsTreeVisualizer to review live executions, or use jtlWriter with withAllFields() or saveAsXml(true) and saveResponseData(true) to generate a jtl file for later analysis.

`,8),Pa={class:"custom-container tip"},Sa=n("p",{class:"custom-container-title"},"TIP",-1),Ia=n("code",null,"debugPostProcessor",-1),Da=n("code",null,"debugPostProcessor",-1),Ea={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/postprocessors/DslDebugPostProcessor.java",target:"_blank",rel:"noopener noreferrer"},Ja=e('

Debug JMeter code

You can even add breakpoints to JMeter code in your IDE and debug the code line by line providing the greatest possible detail.

Here is an example screenshot debugging HTTP Sampler:

JMeter HTTP Sampler debugging in IDE

TIP

JMeter class in charge of executing threads logic is org.apache.jmeter.threads.JMeterThread. You can check the classes used by each DSL-provided test element by checking the DSL code.

Debug Groovy code

',6),Ca={href:"https://github.com/abstracta/jmeter-java-dsl/issues/41#issuecomment-966715062",target:"_blank",rel:"noopener noreferrer"},Aa=n("h3",{id:"dummy-sampler",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#dummy-sampler","aria-hidden":"true"},"#"),s(" Dummy sampler")],-1),Ma=n("code",null,"dummySampler",-1),Oa={href:"https://jmeter-plugins.org/wiki/DummySampler/",target:"_blank",rel:"noopener noreferrer"},La=e(`

Here is an example:

import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    String usersIdVarName = "USER_IDS";
+    String userIdVarName = "USER_ID";
+    String usersPath = "/users";
+    testPlan(
+        httpDefaults().url("http://my.service"),
+        threadGroup(1, 1,
+            // httpSampler(usersPath)
+            dummySampler("[{\\"id\\": 1, \\"name\\": \\"John\\"}, {\\"id\\": 2, \\"name\\": \\"Jane\\"}]")
+                .children(
+                    jsonExtractor(usersIdVarName, "[].id")
+                        .matchNumber(-1)
+                ),
+            forEachController(usersIdVarName, userIdVarName,
+                // httpSampler(usersPath + "/\${" + userIdVarName + "}")
+                dummySampler("{\\"name\\": \\"John or Jane\\"}")
+                    .url("http://my.service/" + usersPath + "/\${" + userIdVarName + "}")
+            )
+        ),
+        resultsTreeVisualizer()
+    ).run();
+  }
+
+}
+

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 responseTimeSimulation() method.

`,3),Ra={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/samplers/DslDummySampler.java",target:"_blank",rel:"noopener noreferrer"},Ga=e(`

Test plan review in JMeter GUI

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 showInGui() method in a test plan to open JMeter GUI with the preloaded test plan.

This can be also used to debug the test plan, by adding elements (like view results tree, dummy samplers, debug post-processors, etc.) in the GUI and running the test plan.

Here is a simple example using the method:

import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        )
+    ).showInGui();
+  }
+
+}
+

Which ends up opening a window like this one:

Test plan in JMeter GUI

Reporting

Once you have a test plan you would usually want to be able to analyze the collected information. This section contains several ways to achieve this.

Log requests and responses

The main mechanism provided by JMeter (and jmeter-java-dsl) to get information about generated requests, responses, and associated metrics is through the generation of JTL files.

This can be easily achieved in jmeter-java-dsl by using provided jtlWriter like in this example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        jtlWriter("target/jtls")
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,13),Na={class:"custom-container tip"},za=n("p",{class:"custom-container-title"},"TIP",-1),Ua=n("code",null,"jtlWriter",-1),Ba=n("code",null,"jtlWriter",-1),Ha=n("code",null,"withAllFields()",-1),Wa=n("code",null,"jtlWriter",-1),Fa={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/listeners/JtlWriter.java",target:"_blank",rel:"noopener noreferrer"},Va=e(`

TIP

By default, jtlWriter will log every sample result, but in some cases you might want to log additional info when a sample result fails. In such scenarios you can use two jtlWriter instances like in this example:

testPlan(
+    threadGroup(2, 10,
+        httpSampler("http://my.service")
+    ),
+    jtlWriter("target/jtls/success")
+      .logOnly(SampleStatus.SUCCESS),
+    jtlWriter("target/jtls/error")
+      .logOnly(SampleStatus.ERROR)
+      .withAllFields(true)
+)
+

TIP

jtlWriter will automatically generate .jtl files applying this format: <yyyy-MM-dd HH-mm-ss> <UUID>.jtl.

If you need a specific file name, for example for later postprocessing logic (eg: using CI build ID), you can specify it by using jtlWriter(directory, fileName).

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 responseFileSaver which automatically generates a file for each received response. Here is an example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        responseFileSaver(Instant.now().toString().replace(":", "-") + "-response")
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,4),Ya={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/listeners/ResponseFileSaver.java",target:"_blank",rel:"noopener noreferrer"},$a=e(`

Finally, if you have more specific needs that are not covered by previous examples, you can use jsr223PostProcessor to define your own custom logic like this:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+                .children(jsr223PostProcessor(
+                    "new File('traceFile') << \\"\${prev.sampleLabel}>>\${prev.responseDataAsString}\\\\n\\""))
+        )
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,2),Xa={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/postprocessors/DslJsr223PostProcessor.java",target:"_blank",rel:"noopener noreferrer"},Ka=n("h3",{id:"real-time-metrics-visualization-and-historic-data-storage",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#real-time-metrics-visualization-and-historic-data-storage","aria-hidden":"true"},"#"),s(" Real-time metrics visualization and historic data storage")],-1),Qa=n("p",null,"When running tests with JMeter (and in particular with jmeter-java-dsl) a usual requirement is to be able to store such test runs in a persistent database to, later on, review such metrics, and compare different test runs. Additionally, jmeter-java-dsl only provides some summary data of a test run in the console while it is running, but, since it doesn't provide any sort of UI, this doesn't allow you to easily analyze such information as it can be done in JMeter GUI.",-1),Za=n("h4",{id:"influxdb",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#influxdb","aria-hidden":"true"},"#"),s(" InfluxDB")],-1),nt={href:"https://www.influxdata.com/products/influxdb-overview/",target:"_blank",rel:"noopener noreferrer"},st={href:"https://www.elastic.co/what-is/elasticsearch",target:"_blank",rel:"noopener noreferrer"},at={href:"https://grafana.com/",target:"_blank",rel:"noopener noreferrer"},tt=n("p",null,[n("img",{src:q,alt:"Grafana JMeter Dashboard Example"})],-1),et=n("code",null,"influxDbListener",-1),pt={href:"https://grafana.com/grafana/dashboards/4026",target:"_blank",rel:"noopener noreferrer"},ot=e(`

Here is an example test plan:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        influxDbListener("http://localhost:8086/write?db=jmeter")
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,2),ct=n("code",null,"docker-compose up",-1),it={href:"https://docs.docker.com/get-docker/",target:"_blank",rel:"noopener noreferrer"},lt={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/docs/guide/reporting/real-time/influxdb",target:"_blank",rel:"noopener noreferrer"},ut={href:"http://localhost:3000",target:"_blank",rel:"noopener noreferrer"},rt=n("code",null,"influxDbListener",-1),kt=n("div",{class:"custom-container warning"},[n("p",{class:"custom-container-title"},"WARNING"),n("p",null,[s("Use the provided "),n("code",null,"docker-compose"),s(" settings for local tests only. It uses weak credentials and is not properly configured for production purposes.")])],-1),dt={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/listeners/InfluxDbBackendListener.java",target:"_blank",rel:"noopener noreferrer"},mt=e(`

Graphite

In a similar fashion to InfluxDB, you can use Graphite and Grafana. Here is an example test plan using the graphiteListener:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        graphiteListener("localhost:2004")
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,3),vt=n("code",null,"docker-compose up",-1),ht={href:"https://docs.docker.com/get-docker/",target:"_blank",rel:"noopener noreferrer"},bt={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/docs/guide/reporting/real-time/graphite",target:"_blank",rel:"noopener noreferrer"},gt=e('

WARNING

Use the provided docker-compose settings for local tests only. It uses weak credentials and is not properly configured for production purposes.

WARNING

graphiteListener is configured to use Pickle Protocol, and port 2004, by default. This is more efficient than text plain protocol, which is the one used by default by JMeter.

Elasticsearch

',3),ft=n("code",null,"jmeter-java-dsl-elasticsearch-listener",-1),yt={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/docs/guide/reporting/real-time/elasticsearch/grafana-provisioning/dashboards/jmeter.json",target:"_blank",rel:"noopener noreferrer"},wt=n("p",null,"To use the module, you will need to include the following dependency in your project:",-1),jt=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-elasticsearch-listener"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),_t=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("repositories "),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token punctuation"},"..."),s(` + maven `),n("span",{class:"token punctuation"},"{"),s(" url "),n("span",{class:"token string"},"'https://jitpack.io'"),s(),n("span",{class:"token punctuation"},"}"),s(` +`),n("span",{class:"token punctuation"},"}"),s(` + +dependencies `),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token punctuation"},"..."),s(` + testImplementation `),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-elasticsearch-listener:1.29'"),s(` +`),n("span",{class:"token punctuation"},"}"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),qt=e(`

And use provided elasticsearchListener() method like in this example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+import static us.abstracta.jmeter.javadsl.elasticsearch.listener.ElasticsearchBackendListener.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        elasticsearchListener("http://localhost:9200/jmeter")
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,2),Tt={class:"custom-container warning"},xt=n("p",{class:"custom-container-title"},"WARNING",-1),Pt={href:"https://github.com/delirius325/jmeter-elasticsearch-backend-listener",target:"_blank",rel:"noopener noreferrer"},St={href:"https://github.com/delirius325/jmeter-elasticsearch-backend-listener/pull/109",target:"_blank",rel:"noopener noreferrer"},It={href:"https://github.com/delirius325/jmeter-elasticsearch-backend-listener/pull/110",target:"_blank",rel:"noopener noreferrer"},Dt=n("code",null,"docker-compose up",-1),Et={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/docs/guide/reporting/real-time/elasticsearch",target:"_blank",rel:"noopener noreferrer"},Jt=n("a",{href:"#influxdb"},"as described for InfluxDB",-1),Ct=n("div",{class:"custom-container warning"},[n("p",{class:"custom-container-title"},"WARNING"),n("p",null,[s("Use provided "),n("code",null,"docker-compose"),s(" settings for local tests only. It uses weak or no credentials and is not properly configured for production purposes.")])],-1),At={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-elasticsearch-listener/src/main/java/us/abstracta/jmeter/javadsl/elasticsearch/listener/ElasticsearchBackendListener.java",target:"_blank",rel:"noopener noreferrer"},Mt=n("h4",{id:"prometheus",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#prometheus","aria-hidden":"true"},"#"),s(" Prometheus")],-1),Ot=n("p",null,"As in previous scenarios, you can also use Prometheus and Grafana.",-1),Lt=n("p",null,"To use the module, you will need to include the following dependency in your project:",-1),Rt=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-prometheus"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),Gt=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-prometheus:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),Nt=e(`

And use provided prometheusListener() method like in this example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+import static us.abstracta.jmeter.javadsl.prometheus.DslPrometheusListener.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        prometheusListener()
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,2),zt=n("code",null,"docker-compose up",-1),Ut={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/docs/guide/reporting/real-time/prometheus",target:"_blank",rel:"noopener noreferrer"},Bt=n("div",{class:"custom-container warning"},[n("p",{class:"custom-container-title"},"WARNING"),n("p",null,[s("Use the provided "),n("code",null,"docker-compose"),s(" settings for local tests only. It uses weak credentials and is not properly configured for production purposes.")])],-1),Ht={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-prometheus/src/main/java/us/abstracta/jmeter/javadsl/prometheus/DslPrometheusListener.java",target:"_blank",rel:"noopener noreferrer"},Wt=e(`

Here is an example that shows the default settings used by prometheusListener:

import us.abstracta.jmeter.javadsl.prometheus.DslPrometheusListener.PrometheusMetric;
+...
+prometheusListener()
+  .metrics(
+    PrometheusMetric.responseTime("ResponseTime", "the response time of samplers")
+      .labels(PrometheusMetric.SAMPLE_LABEL, PrometheusMetric.RESPONSE_CODE)
+      .quantile(0.75, 0.5)
+      .quantile(0.95, 0.1)
+      .quantile(0.99, 0.01)
+      .maxAge(Duration.ofMinutes(1)),
+    PrometheusMetric.successRatio("Ratio", "the success ratio of samplers")
+      .labels(PrometheusMetric.SAMPLE_LABEL, PrometheusMetric.RESPONSE_CODE)
+  )
+  .port(9270)
+  .host("0.0.0.0")
+  .endWait(Duration.ofSeconds(10))
+...
+

Note that the default settings are different from the used JMeter Prometheus Plugin, to allow easier usage and avoid missing metrics at the end of test plan execution.

TIP

When configuring the prometheusListener always consider setting a endWait that is greater thant the Prometheus Server configured scrape_interval to avoid missing metrics at the end of test plan execution (e.g.: 2x the scrape interval value).

DataDog

`,5),Ft=n("code",null,"jmeter-java-dsl-datadog",-1),Vt={href:"https://github.com/DataDog/jmeter-datadog-backend-listener",target:"_blank",rel:"noopener noreferrer"},Yt={href:"https://app.datadoghq.com/integrations/jmeter?search=jmeter",target:"_blank",rel:"noopener noreferrer"},$t=n("p",null,[n("img",{src:T,alt:"datadog jmeter dashboard"})],-1),Xt=n("p",null,"To use the module, just include the dependency:",-1),Kt=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-datadog"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),Qt=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("repositories "),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token punctuation"},"..."),s(` + maven `),n("span",{class:"token punctuation"},"{"),s(" url "),n("span",{class:"token string"},"'https://jitpack.io'"),s(),n("span",{class:"token punctuation"},"}"),s(` +`),n("span",{class:"token punctuation"},"}"),s(` + +dependencies `),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token punctuation"},"..."),s(` + testImplementation `),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-datadog:1.29'"),s(` +`),n("span",{class:"token punctuation"},"}"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),Zt=e(`

And use provided datadogListener() method like in this example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+import static us.abstracta.jmeter.javadsl.datadog.DatadogBackendListener.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        datadogBackendListener(System.getenv("DATADOG_APIKEY"))
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

TIP

If you use a DataDog instance in a site different than US1 (the default one), you can use .site(DatadogSite) method to select the proper site.

TIP

You can use .resultsLogs(true) to send results samples as logs to DataDog to get more information in DataDog on each sample of the test plan (for example for tracing). Enabling this property requires additional network traffic, that may affect test plan execution, and costs on DataDog, so use it sparingly.

`,4),ne={class:"custom-container tip"},se=n("p",{class:"custom-container-title"},"TIP",-1),ae=n("code",null,".tags()",-1),te={href:"https://docs.datadoghq.com/getting_started/tagging/",target:"_blank",rel:"noopener noreferrer"},ee=e(`

Generate HTML reports from test plan execution

After running a test plan you would usually like to visualize the results in a friendly way that eases the analysis of collected information.

One, and preferred way, to do that is through previously mentioned alternatives.

Another way might just be using previously introduced jtlWriter and then loading the jtl file in JMeter GUI with one of JMeter provided listeners (like view results tree, summary report, etc.).

Another alternative is generating a standalone report for the test plan execution using jmeter-java-dsl provided htmlReporter like this:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+        ),
+        htmlReporter("reports")
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

WARNING

htmlReporter will create one directory for each generated report by applying the following template: <yyyy-MM-dd HH-mm-ss> <UUID>.

If you need a particular name for the report directory, for example for postprocessing logic (eg: adding CI build ID), you can use htmlReporter(reportsDirectory, name) to specify the name.

Make sure when specifying the name, for it to be unique, otherwise report generation will fail after test plan execution.

TIP

Time graphs by default group metrics per minute, but you can change this with provided timeGraphsGranularity method.

Live built-in graphs and stats

Sometimes you want to get live statistics on the test plan and don't want to install additional tools, and are not concerned about keeping historic data.

You can use dashboardVisualizer to get live charts and stats for quick review.

To use it, you need to add the following dependency:

`,12),pe=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-dashboard"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),oe=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-dashboard:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),ce=e(`

And use it as you would with any of the previously mentioned listeners (like influxDbListener and jtlWriter).

Here is an example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+import static us.abstracta.jmeter.javadsl.dashboard.DashboardVisualizer.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup("Group1")
+            .rampToAndHold(10, Duration.ofSeconds(10), Duration.ofSeconds(10))
+            .children(
+                httpSampler("Sample 1", "http://my.service")
+            ),
+        threadGroup("Group2")
+            .rampToAndHold(20, Duration.ofSeconds(10), Duration.ofSeconds(20))
+            .children(
+                httpSampler("Sample 2", "http://my.service/get")
+            ),
+        dashboardVisualizer()
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

The dashboardVisualizer will pop up a window like the following one, which you can use to trace statistics while the test plan runs:

Dashboard example window

WARNING

The dashboard imposes additional resources (CPU & RAM) consumption on the machine generating the load test, which may affect the test plan execution and reduce the number of concurrent threads you may reach in your machine. In general, prefer using one of the previously mentioned methods and using the dashboard just for local testing and quick feedback.

Remember to remove it when is no longer needed in the test plan

WARNING

The test will not end until you close all popup windows. This allows you to see the final charts and statistics of the plan before ending the test.

TIP

As with jtlWriter and influxDbListener, you can place dashboardVisualizer at different levels of the test plan (at the test plan level, at the thread group level, as a child of a sampler, etc.), to only capture statistics of that particular part of the test plan.

Response processing

Check for expected response

By default, JMeter marks any HTTP request with a fail response code (4xx or 5xx) as failed, which allows you to easily identify when some request unexpectedly fails. But in many cases, this is not enough or desirable, and you need to check for the response body (or some other field) to contain (or not) a certain string.

This is usually accomplished in JMeter with the usage of Response Assertions, which provides an easy and fast way to verify that you get the proper response for each step of the test plan, marking the request as a failure when the specified condition is not met.

Here is an example of how to specify a response assertion in jmeter-java-dsl:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+                .children(
+                    responseAssertion().containsSubstrings("OK")
+                )
+        )
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+
`,14),ie={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/assertions/DslResponseAssertion.java",target:"_blank",rel:"noopener noreferrer"},le=e(`

For more complex scenarios check following section.

Check for expected JSON

When checking for JSON responses, it is usually easier to just use jsonAssertion. Here is an example:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.apache.http.entity.ContentType;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service/accounts")
+                .post("{\\"name\\": \\"John Doe\\"}", ContentType.APPLICATION_JSON)
+                .children(
+                    jsonAssertion("id")
+                ),
+            httpSampler("http://my.service/accounts/\${ACCOUNT_ID}")
+        )
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

TIP

Previous example just checks that sample result JSON contains an id field. You can use matches(regex), equalsTo(value) or even equalsToJson(json) methods to check id associated value. Additionally, you can use the not() method to check for the inverse condition. E.g.: does not contain id field, or field value does not match a given regular expression or is not equal to a given value.

`,5),ue={class:"custom-container tip"},re=n("p",{class:"custom-container-title"},"TIP",-1),ke={href:"https://jmespath.org/",target:"_blank",rel:"noopener noreferrer"},de={href:"https://github.com/json-path/JsonPath",target:"_blank",rel:"noopener noreferrer"},me=n("code",null,".queryLanguage(JsonQueryLanguage.JSON_PATH)",-1),ve=e(`

Change sample result statuses with custom logic

Sometimes response assertions and JMeter default behavior are not enough, and custom logic is required. In such scenarios you can use jsr223PostProcessor as in this example where the 429 status code is not considered as a fail status code:

import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class PerformanceTest {
+
+  @Test
+  public void testPerformance() throws IOException {
+    TestPlanStats stats = testPlan(
+        threadGroup(2, 10,
+            httpSampler("http://my.service")
+                .children(
+                    jsr223PostProcessor(
+                        "if (prev.responseCode == '429') { prev.successful = true }")
+                )
+        )
+    ).run();
+    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
+  }
+
+}
+

You can also use a Java lambda instead of providing Groovy script, which benefits from Java type safety & IDEs code auto-completion and consumes less CPU:

jsr223PostProcessor(s -> {
+    if ("429".equals(s.prev.getResponseCode())) {
+      s.prev.setSuccessful(true);
+    }
+})
+

WARNING

Even though using Java Lambdas has several benefits, they are also less portable. Check following section for more details.

`,6),he={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/postprocessors/DslJsr223PostProcessor.java",target:"_blank",rel:"noopener noreferrer"},be=e('

WARNING

JSR223PostProcessor is a very powerful tool but is not the only, nor the best, alternative for many cases where JMeter already provides a better and simpler alternative. For instance, the previous example might be implemented with previously presented Response Assertion.

Lambdas

',2),ge={href:"https://github.com/abstracta/jmeter-java-dsl/releases/tag/v1.14",target:"_blank",rel:"noopener noreferrer"},fe=e('

But, they are also less portable.

For instance, they will not work out of the box with remote engines (like BlazeMeterEngine) or while saving JMX and running it in standalone JMeter.

One option is using groovy scripts and __groovy function, but doing so, you lose the previously mentioned benefits.

Here is another approach to still benefit from Java code (vs Groovy script) and run in remote engines and standalone JMeter.

Lambdas in remote engine

Here are the steps to run test plans containing Java lambdas in BlazeMeterEngine:

',6),ye=e(`
  • Replace all Java lambdas with public static classes implementing proper script interface.

    For example, if you have the following test:

    public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +                .children(
    +                    jsr223PostProcessor(s -> {
    +                      if ("429".equals(s.prev.getResponseCode())) {
    +                        s.prev.setSuccessful(true);
    +                      }
    +                    })
    +                )
    +        )
    +    ).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN")));
    +  }
    +  
    +}
    +

    You can change it to:

    public class PerformanceTest {
    +  
    +  public static class StatusSuccessProcessor implements PostProcessorScript {
    +
    +    @Override
    +    public void runScript(PostProcessorVars s) {
    +      if ("429".equals(s.prev.getResponseCode())) {
    +        s.prev.setSuccessful(true);
    +      }
    +    }
    +    
    +  }
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +                .children(
    +                    jsr223PostProcessor(StatusSuccessProcessor.class)
    +                )
    +        )
    +    ).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN")));
    +  }
    +
    +}
    +

    Script interface to implement, depends on where you use the lambda code. Available interfaces are PropertyScript, PreProcessorScript, PostProcessorScript, and SamplerScript.

  • `,1),we=e(`

    Upload your test code and dependencies to BlazeMeter.

    If you use maven, here is what you can add to your project to configure this:

    <plugins>
    +  ...
    +  <!-- this generates a jar containing your test code (including the public static class previously mentioned) -->
    +  <plugin>
    +     <groupId>org.apache.maven.plugins</groupId>
    +     <artifactId>maven-jar-plugin</artifactId>
    +     <version>3.3.0</version>
    +     <executions>
    +       <execution>
    +         <goals>
    +           <goal>test-jar</goal>
    +         </goals>
    +       </execution>
    +     </executions>
    +  </plugin>
    +  <!-- this copies project dependencies to target/libs directory -->
    +  <plugin>
    +    <groupId>org.apache.maven.plugins</groupId>
    +    <artifactId>maven-dependency-plugin</artifactId>
    +    <version>3.6.0</version>
    +    <executions>
    +      <execution>
    +        <id>copy-dependencies</id>
    +        <phase>package</phase>
    +        <goals>
    +          <goal>copy-dependencies</goal>
    +        </goals>
    +        <configuration>
    +          <outputDirectory>\${project.build.directory}/libs</outputDirectory>
    +          <!-- include here, separating by commas, any additional dependencies (just the artifacts ids) you need to upload to BlazeMeter -->
    +          <!-- AzureEngine automatically uploads JMeter dsl artifacts, so only transitive or custom dependencies would be required -->
    +          <!-- if you would like for BlazeMeterEngine and OctoPerfEngine to automatically upload JMeter DSL artifacts, please create an issue in GitHub repository -->
    +          <includeArtifactIds>jmeter-java-dsl</includeArtifactIds>
    +        </configuration>
    +      </execution>
    +    </executions>
    +  </plugin>
    +  <!-- this takes care of executing tests classes ending with IT after test jar is generated and dependencies are copied -->
    +  <!-- additionally, it sets some system properties as to easily identify test jar file -->
    +  <plugin>
    +    <groupId>org.apache.maven.plugins</groupId>
    +    <artifactId>maven-failsafe-plugin</artifactId>
    +    <version>3.0.0-M7</version>
    +    <configuration>
    +      <systemPropertyVariables>
    +        <testJar.path>\${project.build.directory}/\${project.artifactId}-\${project.version}-tests.jar</testJar.path> 
    +      </systemPropertyVariables>
    +    </configuration>
    +    <executions>
    +      <execution>
    +        <goals>
    +          <goal>integration-test</goal>
    +          <goal>verify</goal>
    +        </goals>
    +      </execution>
    +    </executions>
    +  </plugin>
    +</plugins>
    +

    Additionally, rename your test class to use IT suffix (so it runs after test jar is created and dependencies are copied), and add to BlazeMeterEngine logic to upload the jars. For example:

    // Here we renamed from PerformanceTest to PerformanceIT
    +public class PerformanceIT {
    +  
    +   ...
    +   
    +   @Test
    +   public void testPerformance() throws Exception {
    +      testPlan(
    +              ...
    +      ).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN"))
    +              .assets(findAssets()));
    +   }
    +
    +   private File[] findAssets() {
    +      File[] libsFiles = new File("target/libs").listFiles();
    +      File[] ret = new File[libsFiles.length + 1];
    +      ret[0] = new File(System.getProperty("testJar.path"));
    +      System.arraycopy(libsFiles, 0, ret, 1, libsFiles.length);
    +      return ret;
    +   }
    +
    +}
    +
    `,5),je={class:"custom-container tip"},_e=n("p",{class:"custom-container-title"},"TIP",-1),qe=n("code",null,"BlazeMeterEngine",-1),Te=n("code",null,"AzureEngine",-1),xe={href:"https://github.com/abstracta/issues",target:"_blank",rel:"noopener noreferrer"},Pe=n("li",null,[n("p",null,[s("Execute your tests with maven (either with "),n("code",null,"mvn clean verify"),s(" or as part of "),n("code",null,"mvn clean install"),s(") or IDE (by first packaging your project, and then executing "),n("code",null,"PerformanceIT"),s(" test).")])],-1),Se=e(`
    Lambdas in standalone JMeter

    If you save your test plan with the saveAsJmx() test plan method and then want to execute the test plan in JMeter, you will need to:

    1. Replace all Java lambdas with public static classes implementing proper script interface.

      Same as the previous section.

    2. Package your test code in a jar.

      Same as the previous section.

    3. Copy all dependencies, in addition to jmeter-java-dsl, required by the lambda code to JMeter lib/ext folder.

      You can also use maven-dependency-plugin and run mvn package -DskipTests to get the actual jars. If the test plan requires any particular jmeter plugin, then you would need to copy those as well.

    Use part of a response in a subsequent request (aka correlation)

    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.

    Regular expressions extraction

    Here is an example with jmeter-java-dsl using regular expressions:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\\"name\\": \\"John Doe\\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    regexExtractor("ACCOUNT_ID", "\\"id\\":\\"([^\\"]+)\\"")
    +                ),
    +            httpSampler("http://my.service/accounts/\${ACCOUNT_ID}")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +
    `,8),Ie={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/postprocessors/DslRegexExtractor.java",target:"_blank",rel:"noopener noreferrer"},De=e(`

    Boundaries based extraction

    Regular expressions are quite powerful and flexible, but also are complex and performance might not be optimal in some scenarios. When you know that desired extraction is always surrounded by some specific text that never varies, then you can use boundaryExtractor which is simpler and in many cases more performant:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\\"name\\": \\"John Doe\\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    boundaryExtractor("ACCOUNT_ID", "\\"id\\":\\"", "\\"")
    +                ),
    +            httpSampler("http://my.service/accounts/\${ACCOUNT_ID}")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +
    `,3),Ee={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/postprocessors/DslBoundaryExtractor.java",target:"_blank",rel:"noopener noreferrer"},Je=e(`

    JSON extraction

    When the response of a request is JSON, then you can use jsonExtractor like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\\"name\\": \\"John Doe\\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    jsonExtractor("ACCOUNT_ID", "id")
    +                ),
    +            httpSampler("http://my.service/accounts/\${ACCOUNT_ID}")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +
    `,3),Ce={class:"custom-container tip"},Ae=n("p",{class:"custom-container-title"},"TIP",-1),Me={href:"https://jmespath.org/",target:"_blank",rel:"noopener noreferrer"},Oe={href:"https://github.com/json-path/JsonPath",target:"_blank",rel:"noopener noreferrer"},Le=n("code",null,".queryLanguage(JsonQueryLanguage.JSON_PATH)",-1),Re=e(`

    Requests generation

    Conditionals

    At some point, you will need to execute part of a test plan according to a certain condition (eg: a value extracted from a previous request). When you reach that point, you can use ifController like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\\"name\\": \\"John Doe\\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    regexExtractor("ACCOUNT_ID", "\\"id\\":\\"([^\\"]+)\\"")
    +                ),
    +            ifController("\${__groovy(vars['ACCOUNT_ID'] != null)}",
    +                httpSampler("http://my.service/accounts/\${ACCOUNT_ID}")
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    You can also use a Java lambda instead of providing JMeter expression, which benefits from Java type safety & IDEs code auto-completion and consumes less CPU:

    ifController(s -> s.vars.get("ACCOUNT_ID") != null,
    +    httpSampler("http://my.service/accounts/\${ACCOUNT_ID}")
    +)
    +

    WARNING

    Even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    `,7),Ge={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/DslIfController.java",target:"_blank",rel:"noopener noreferrer"},Ne={href:"https://jmeter.apache.org/usermanual/component_reference.html#If_Controller",target:"_blank",rel:"noopener noreferrer"},ze=e(`

    Loops

    Iterating over extracted values

    A common use case is to iterate over a list of values extracted from a previous request and execute part of the plan for each extracted value. This can be easily done using foreachController like in the following example:

    package us.abstracta.jmeter.javadsl;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    String productsIdVarName = "PRODUCT_IDS";
    +    String productIdVarName = "PRODUCT_ID";
    +    String productsPath = "/products";
    +    TestPlanStats stats = testPlan(
    +        httpDefaults().url("http://my.service"),
    +        threadGroup(2, 10,
    +            httpSampler(productsPath)
    +                .children(
    +                    jsonExtractor(productsIdVarName, "[].id")
    +                        .matchNumber(-1)
    +                ),
    +            forEachController(productsIdVarName, productIdVarName,
    +                httpSampler(productsPath + "/\${" + productIdVarName + "}")
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    JMeter automatically generates a variable __jm__<loopName>__idx with the current index of the for each iteration (starting with 0), which you can use in controller children elements if needed. The default name for the for each controller, when not specified, is foreach.

    `,5),Ue={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/DslForEachController.java",target:"_blank",rel:"noopener noreferrer"},Be=n("h4",{id:"iterating-while-a-condition-is-met",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#iterating-while-a-condition-is-met","aria-hidden":"true"},"#"),s(" Iterating while a condition is met")],-1),He=n("code",null,"whileController",-1),We={href:"https://jmeter.apache.org/usermanual/component_reference.html#While_Controller",target:"_blank",rel:"noopener noreferrer"},Fe=e(`
    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            whileController("\${__groovy(vars['ACCOUNT_ID'] == null)}",
    +                httpSampler("http://my.service/accounts")
    +                    .post("{\\"name\\": \\"John Doe\\"}", ContentType.APPLICATION_JSON)
    +                    .children(
    +                        regexExtractor("ACCOUNT_ID", "\\"id\\":\\"([^\\"]+)\\"")
    +                    )
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    As with ifController, you can also use Java lambdas to benefit from IDE auto-completion and type safety and less CPU consumption. Eg:

    whileController(s -> s.vars.get("ACCOUNT_ID") == null,
    +    httpSampler("http://my.service/accounts")
    +      .post("{\\"name\\": \\"John Doe\\"}", Type.APPLICATION_JSON)
    +      .children(
    +        regexExtractor("ACCOUNT_ID", "\\"id\\":\\"([^\\"]+)\\"")
    +      )
    +)
    +

    WARNING

    Even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    WARNING

    JMeter evaluates while conditions before entering each iteration, and after exiting each iteration. Take this into consideration if the condition has side effects (eg: incrementing counters, altering some other state, etc).

    TIP

    JMeter automatically generates a variable __jm__<loopName>__idx with the current index of while iteration (starting with 0). Example:

    whileController("items", "\${__groovy(vars.getObject('__jm__items__idx') < 4)}",
    +    httpSampler("http://my.service/items")
    +      .post("{\\"name\\": \\"My Item\\"}", Type.APPLICATION_JSON)
    +)
    +

    The default name for the while controller, when not specified, is while.

    `,6),Ve={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/DslWhileController.java",target:"_blank",rel:"noopener noreferrer"},Ye=n("h4",{id:"iterating-a-fixed-number-of-times",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#iterating-a-fixed-number-of-times","aria-hidden":"true"},"#"),s(" Iterating a fixed number of times")],-1),$e=n("code",null,"forLoopController",-1),Xe={href:"https://jmeter.apache.org/usermanual/component_reference.html#Loop_Controller",target:"_blank",rel:"noopener noreferrer"},Ke=e(`
    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            forLoopController(5,
    +                httpSampler("http://my.service/accounts")
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    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 __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.

    `,3),Qe={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/ForLoopController.java",target:"_blank",rel:"noopener noreferrer"},Ze=e(`

    Iterating for a given period

    In some scenarios you might want to execute a given logic until all the steps are executed or a given period of time has passed. In these scenarios you can use runtimeController which stops executing children elements when a specified time is reached.

    Here is an example which makes requests to a page until token expires by using runtimeController in combination with whileController.

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    Duration tokenExpiration = Duration.ofSeconds(5);
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/token"),
    +            runtimeController(tokenExpiration,
    +                whileController("true",
    +                    httpSampler("http://my.service/accounts")
    +                )
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +
    `,4),np={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/DslRuntimeController.java",target:"_blank",rel:"noopener noreferrer"},sp=n("h4",{id:"execute-only-once-in-thread",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#execute-only-once-in-thread","aria-hidden":"true"},"#"),s(" Execute only once in thread")],-1),ap=n("code",null,"onceOnlyController",-1),tp={href:"https://jmeter.apache.org/usermanual/component_reference.html#Once_Only_Controller",target:"_blank",rel:"noopener noreferrer"},ep=e(`

    You can use this, for example, for one-time authorization or for setting JMeter variables or properties.

    Here is an example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.JmeterDslTest;
    +
    +public class DslOnceOnlyControllerTest extends JmeterDslTest {
    +
    +  @Test
    +  public void shouldExecuteOnlyOneTimeWhenOnceOnlyControllerInPlan() throws Exception {
    +    testPlan(
    +        threadGroup(1, 10,
    +            onceOnlyController(
    +                httpSampler("http://my.service/login") // only runs once
    +                    .method(HTTPConstants.POST)
    +                    .header("Authorization", "Basic asdf=")
    +                    .children(
    +                        regexExtractor("AUTH_TOKEN", "authToken=(.*)")
    +                    )
    +            ),
    +            httpSampler("http://my.service/accounts") // runs ten times
    +                .header("Authorization", "Bearer \${AUTH_TOKEN}")
    +        )
    +    ).run();
    +  }
    +
    +}
    +
    `,3),pp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/DslOnceOnlyController.java",target:"_blank",rel:"noopener noreferrer"},op=e(`

    Group requests

    Sometimes, is necessary to be able to group requests which constitute different steps in a test. For example, to separate necessary requests to do a login from the ones used to add items to the cart and the ones to do a purchase. JMeter (and the DSL) provide Transaction Controllers for this purpose, here is an example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testTransactions() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            transaction('login',
    +                httpSampler("http://my.service"),
    +                httpSampler("http://my.service/login")
    +                    .post("user=test&password=test", ContentType.APPLICATION_FORM_URLENCODED)
    +            ),
    +            transaction('addItemToCart',
    +                httpSampler("http://my.service/items"),
    +                httpSampler("http://my.service/cart/items")
    +                    .post("{\\"id\\": 1}", ContentType.APPLICATION_JSON)
    +            )
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    This will provide additional sample results for each transaction, which contain the aggregate metrics for containing requests, allowing you to focus on the actual flow steps instead of each particular request.

    If you don't want to generate additional sample results (and statistics), and want to group requests for example to apply a given timer, config, assertion, listener, pre- or post-processor, then you can use simpleController like in following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testTransactions() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            simpleController('login',
    +                httpSampler("http://my.service"),
    +                httpSampler("http://my.service/users"),
    +                responseAssertion()
    +                  .containsSubstrings("OK")
    +            )
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    You can even use transactionController and simpleController to easily modularize parts of your test plan into Java methods (or classes) like in this example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.controllers.DslTransactionController;
    +
    +public class PerformanceTest {
    +
    +  private DslTransactionController login(String baseUrl) {
    +    return transaction("login",
    +        httpSampler(baseUrl),
    +        httpSampler(baseUrl + "/login")
    +            .post("user=test&password=test", ContentType.APPLICATION_FORM_URLENCODED)
    +    );
    +  }
    +
    +  private DslTransactionController addItemToCart(String baseUrl) {
    +    return transaction("addItemToCart",
    +        httpSampler(baseUrl + "/items"),
    +        httpSampler(baseUrl + "/cart/items")
    +            .post("{\\"id\\": 1}", ContentType.APPLICATION_JSON)
    +    );
    +  }
    +
    +  @Test
    +  public void testTransactions() throws IOException {
    +    String baseUrl = "http://my.service";
    +    testPlan(
    +        threadGroup(2, 10,
    +            login(baseUrl),
    +            addItemToCart(baseUrl)
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    CSV as input data for requests

    Sometimes is necessary to run the same flow but using different pre-defined data on each request. For example, a common use case is to use a different user (from a given set) in each request.

    This can be easily achieved using the provided csvDataSet element. For example, having a file like this one:

    USER,PASS
    +user1,pass1
    +user2,pass2
    +

    You can implement a test plan that tests recurrent login with the two users with something like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        csvDataSet("users.csv"),
    +        threadGroup(5, 10,
    +            httpSampler("http://my.service/login")
    +                .post("{\\"\${USER}\\": \\"\${PASS}\\"", ContentType.APPLICATION_JSON),
    +            httpSampler("http://my.service/logout")
    +                .method(HTTPConstants.POST)
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    To properly format the data in your CSV, a general rule you can apply is to replace each double quotes with two double quotes and add double quotes to the beginning and end of each CSV value.

    E.g.: if you want one CSV field to contain the value {"field": "value"}, then use "{""field:"": ""value""}".

    This way, with a simple search and replace, you can include in a CSV field any format like JSON, XML, etc.

    Note: JMeter uses should be aware that JMeter DSL csvDataSet sets Allowed quoted data? flag, in associated Csv Data Set Config element, to true.

    By default, the CSV file will be opened once and shared by all threads. This means that when one thread reads a CSV line in one iteration, then the following thread reading a line will continue with the following line.

    If you want to change this (to share the file per thread group or use one file per thread), then you can use the provided sharedIn method like in the following example:

    import us.abstracta.jmeter.javadsl.core.configs.DslCsvDataSet.Sharing;
    +...
    +  TestPlanStats stats = testPlan(
    +      csvDataSet("users.csv")
    +        .sharedIn(Sharing.THREAD),
    +      threadGroup(5, 10,
    +          httpSampler("http://my.service/login")
    +            .post("{\\"\${USER}\\": \\"\${PASS}\\"", Type.APPLICATION_JSON),
    +          httpSampler("http://my.service/logout")
    +            .method(HTTPConstants.POST)
    +      )
    +  )
    +

    :::

    `,19),cp={class:"custom-container warning"},ip=n("p",{class:"custom-container-title"},"WARNING",-1),lp=n("code",null,"randomOrder()",-1),up={href:"https://github.com/Blazemeter/jmeter-bzm-plugins/blob/master/random-csv-data-set/RandomCSVDataSetConfig.md",target:"_blank",rel:"noopener noreferrer"},rp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/configs/DslCsvDataSet.java",target:"_blank",rel:"noopener noreferrer"},kp=e(`

    Counter

    In scenarios that you need unique value for each request, for example for id parameters, you can use counter which provides easy means to have an auto incremental value that can be used in requests.

    Here is an example:

    testPlan(
    +    threadGroup(1, 10,
    +        counter("USER_ID")
    +            .startingValue(1000), // will generate 1000, 1001, 1002...
    +        httpSampler(wiremockUri + "/\${USER_ID}")
    +    )
    +).run();
    +
    `,4),dp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/configs/DslCounter.java",target:"_blank",rel:"noopener noreferrer"},mp=e(`

    Provide request parameters programmatically per request

    So far we have seen a few ways to generate requests with information extracted from CSV or through a counter, but this is not enough for some scenarios. When you need more flexibility and power you can use jsr223preProcessor to specify your own logic to build each request.

    Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.jmeter.threads.JMeterVariables;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +                .post("\${REQUEST_BODY}", ContentType.TEXT_PLAIN)
    +                .children(
    +                    jsr223PreProcessor("vars.put('REQUEST_BODY', " + getClass().getName()
    +                        + ".buildRequestBody(vars))")
    +                )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +  public static String buildRequestBody(JMeterVariables vars) {
    +    String countVarName = "REQUEST_COUNT";
    +    Integer countVar = (Integer) vars.getObject(countVarName);
    +    int count = countVar != null ? countVar + 1 : 1;
    +    vars.putObject(countVarName, count);
    +    return "MyBody" + count;
    +  }
    +
    +}
    +

    You can also use a Java lambda instead of providing Groovy script, which benefits from Java type safety & IDEs code auto-completion and consumes less CPU:

    jsr223PreProcessor(s -> s.vars.put("REQUEST_BODY", buildRequestBody(s.vars)))
    +

    Or even use this shorthand:

    post(s -> buildRequestBody(s.vars), Type.TEXT_PLAIN)
    +

    WARNING

    Even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    TIP

    jsr223PreProcessor is quite powerful. But, provided example can easily be achieved through the usage of counter element.

    `,10),vp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/preprocessors/DslJsr223PreProcessor.java",target:"_blank",rel:"noopener noreferrer"},hp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/http/DslHttpSampler.java",target:"_blank",rel:"noopener noreferrer"},bp=e(`

    Timers

    Emulate user delays between requests

    Sometimes, is necessary to be able to properly replicate users' behavior, and in particular the time the users take between sending one request and the following one. For example, to simulate the time it will take to complete a purchase form. JMeter (and the DSL) provide a few alternatives for this.

    If you just want to add 1 pause between two requests, you can use the threadPause method like in the following example:

    import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/items"),
    +            threadPause(Duration.ofSeconds(4)),
    +            httpSampler("http://my.service/cart/selected-items")
    +                .post("{\\"id\\": 1}", ContentType.APPLICATION_JSON)
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Using threadPause is a good solution for adding individual pauses, but if you want to add pauses across several requests, or sections of test plan, then using a constantTimer or uniformRandomTimer is better. Here is an example that adds a delay of between 4 and 10 seconds for every request in the test plan:

    import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testTransactions() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            uniformRandomTimer(Duration.ofSeconds(4), Duration.ofSeconds(10)),
    +            transaction("addItemToCart",
    +                httpSampler("http://my.service/items"),
    +                httpSampler("http://my.service/cart/selected-items")
    +                    .post("{\\"id\\": 1}", ContentType.APPLICATION_JSON)
    +            ),
    +            transaction("checkout",
    +                httpSampler("http://my.service/cart/chekout"),
    +                httpSampler("http://my.service/cart/checkout/userinfo")
    +                    .post(
    +                        "{\\"Name\\": Dave, \\"lastname\\": Tester, \\"Street\\": 1483  Smith Road, \\"City\\": Atlanta}",
    +                        ContentType.APPLICATION_JSON)
    +            )
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    TIP

    As you may have noticed, timer order in relation to samplers, doesn't matter. Timers apply to all samplers in their scope, adding a pause after pre-processor executions and before the actual sampling. threadPause order, on the other hand, is relevant, and the pause will only execute when previous samplers in the same scope have run and before following samplers do.

    WARNING

    uniformRandomTimer minimum and maximum parameters differ from the ones used by JMeter Uniform Random Timer element, to make it simpler for users with no JMeter background.

    The generated JMeter test element uses the Constant Delay Offset set to minimum value, and the Maximum random delay set to (maximum - minimum) value.

    Control throughput

    To achieve a specific constant throughput for specific samplers or section of a test plan, you can use throughputTimer, which uses JMeter ConstantThroughputTimer.

    Here is an example for generating a maximum of 120 samples per minute:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    testPlan(
    +        threadGroup(10, Duration.ofSeconds(10),
    +            throughputTimer(120),
    +            httpSampler("http://my.service")
    +        ) 
    +    ).run();
    +  }
    +
    +}
    +

    TIP

    By default, throughtputTimer will control throughput among active threads. If you want to control throughput per thread, i.e. each thread generating the specified throughput, which means that totalThoughput = configuredThroughput * numberOfThreads, you can use perThread() method.

    TIP

    The placement (scope) of the throughputTimer will determine its behaviour. E.g. if you place the timer inside an ifController, it will only control the execution throughput only for elements inside the ifController, or if you place it inside a threadGroup other thread groups execution will be directly not affected (nor they would directly affect this timer execution).

    `,15),gp={class:"custom-container tip"},fp=n("p",{class:"custom-container-title"},"TIP",-1),yp=n("p",null,[s("The timer uses by default even distribution of throughput among the active threads. This means that if you have 10 threads and specify 10 tpm, then each thread will try to execute at 1tpm, not adjusting each thread tpm if some other thread was far from achieving the configured tpm. If you want more precise throughput control, you can use "),n("code",null,".calculation()"),s(" method, for example, with "),n("code",null,"THREAD_GROUP_ACCURATE"),s(", but doing so, may lead to unexpected behavior when using multiple timers in same thread group.")],-1),wp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/timers/DslThroughputTimer.java",target:"_blank",rel:"noopener noreferrer"},jp=e('

    WARNING

    throughputTimer works by pausing requests to achieve a constant throughput, so the response times and number of threads must be sufficient to achieve the target throughput. You can think of this timer as a way to limit the maximum throughput, but it does have no way to generate more load if response times are high and threads are not enough. To automatically adjust threads when response times are high you can use rpsThreadGroup as described here.

    WARNING

    On first invocation of throughputTimer on each thread, no delay will be generated by the timer, which may lead to initially higher throughput than expected.

    For example, in previously provided example, 10 requests (1 for each thread) will run without "throughput control", which means you will get 10 requests at once, and after that, you will get 1 request per second (as expected).

    Requests synchronization

    Usually, samples generated by different threads in a test plan thread group start deviating from each other according to the different durations each of them may experience.

    ',4),_p={href:"https://github.com/abstracta/jmeter-java-dsl/discussions/204",target:"_blank",rel:"noopener noreferrer"},qp=e('

    not synchronized samples in 2 threads and 3 iterations

    In most cases this is ok. But, if you want to generate batches of simultaneous requests to a system under test, this variability will prevent you from getting the expected behavior.

    So, to synchronize requests, by holding some of them until all are in sync, like in this diagram:

    synchronized samples in 2 threads and 3 iterations

    You can use synchronizingTimer like in the following example:

    testPlan(
    +    threadGroup(2, 3,
    +      httpSample("https://mysite"),
    +      synchronizingTimer()
    +    )
    +)
    +

    Execute part of a test plan part a fraction of the times

    In some cases, you may want to execute a given part of the test plan not in every iteration, and only for a given percent of times, to emulate certain probabilistic nature of the flow the users execute.

    In such scenarios, you may use percentController, which uses JMeter Throughput Controller to achieve exactly that.

    Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            percentController(40, // run this 40% of the times
    +                httpSampler("http://my.service/status"),
    +                httpSampler("http://my.service/poll")),
    +            percentController(70, // run this 70% of the times
    +                httpSampler("http://my.service/items"))
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +
    `,11),Tp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/PercentController.java",target:"_blank",rel:"noopener noreferrer"},xp=e(`

    Switch between test plan parts with a given probability

    In some cases, you need to switch in a test plan between different behaviors assigning to them different probabilities. The main difference between this need and the previous one is that in each iteration you have to execute one of the parts, while in the previous case you might get multiple or no part executed on a given iteration.

    For this scenario you can use weightedSwitchCotroller, like in this example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            weightedSwitchController()
    +                .child(30, httpSampler("https://myservice/1")) // will run 30/(30+20)=60% of the iterations
    +                .child(20, httpSampler("https://myservice/2")) // will run 20/(30+20)=40% of the iterations
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +
    `,4),Pp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/DslWeightedSwitchController.java",target:"_blank",rel:"noopener noreferrer"},Sp=n("h3",{id:"parallel-requests",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#parallel-requests","aria-hidden":"true"},"#"),s(" Parallel requests")],-1),Ip=n("code",null,"paralleController",-1),Dp={href:"https://github.com/Blazemeter/jmeter-bzm-plugins/blob/master/parallel/Parallel.md",target:"_blank",rel:"noopener noreferrer"},Ep=n("p",null,"To use it, add the following dependency to your project:",-1),Jp=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-parallel"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),Cp=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-dashboard:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),Ap=e(`

    And use it, like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.parallel.ParallelController.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            parallelController(
    +                httpSampler("http://my.service/status"),
    +                httpSampler("http://my.service/poll"))
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    By default, the controller has no limit on the number of parallel requests per JMeter thread. You can set a limit by using provided maxThreads(int) method. Additionally, you can opt to aggregate children's results in a parent sampler using generateParentSample(boolean) method, in a similar fashion to the transaction controller.

    TIP

    When requesting embedded resources of an HTML response, prefer using downloadEmbeddedResources() method in httpSampler instead. Likewise, when you just need independent parts of a test plan to execute in parallel, prefer using different thread groups for each part.

    `,4),Mp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-parallel/src/main/java/us/abstracta/jmeter/javadsl/parallel/ParallelController.java",target:"_blank",rel:"noopener noreferrer"},Op=e(`

    JMeter variables & properties

    Variables

    In general, when you want to reuse a certain value of your script, you can, and is the preferred way, just to use Java variables. In some cases though, you might need to pre-initialize some JMeter thread variable (for example to later be used in an ifController) or easily update its value without having to use a jsr223 element for that. For these cases, the DSL provides the vars() method.

    Here is an example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    String pageVarName = "PAGE";
    +    String firstPage = "1";
    +    String endPage = "END";
    +    testPlan(
    +        vars()
    +            .set(pageVarName, firstPage),
    +        threadGroup(2, 10,
    +            ifController(s -> !s.vars.get(pageVarName).equals(endPage),
    +                httpSampler("http://my.service/accounts?page=\${" + pageVarName +"}")
    +                    .children(
    +                        regexExtractor(pageVarName, "next=.*?page=(\\\\d+)")
    +                            .defaultValue(endPage)
    +                    )
    +            ),
    +            ifController(s -> s.vars.get(pageVarName).equals(endPage),
    +                vars()
    +                    .set(pageVarName, firstPage)
    +            )
    +        )
    +    ).run();
    +  }
    +
    +}
    +
    `,5),Lp={class:"custom-container warning"},Rp=n("p",{class:"custom-container-title"},"WARNING",-1),Gp=n("p",null,"For special consideration of existing JMeter users:",-1),Np=n("code",null,"vars()",-1),zp={href:"https://jmeter.apache.org/usermanual/component_reference.html#User_Defined_Variables",target:"_blank",rel:"noopener noreferrer"},Up=n("p",null,[s("Internally using a JSR223 sampler, allows DSL users to properly scope a variable to where it is placed (eg: defining a variable in one thread has no effect on other threads or thread groups), set the value when it's actually needed (not just at the beginning of test plan execution), and support cross-variable references (i.e.: if "),n("code",null,"var1=test"),s(" and "),n("code",null,"var2=${var1}"),s(", then the value of "),n("code",null,"var2"),s(" would be solved to "),n("code",null,"test"),s(").")],-1),Bp=n("p",null,[s("When "),n("code",null,"vars()"),s(" is located as a direct child of the test plan, due to the usage of UDV, declared variables will be available to all thread groups and no variable cross-reference is supported.")],-1),Hp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/configs/DslVariables.java",target:"_blank",rel:"noopener noreferrer"},Wp=e(`

    Properties

    You might reach a point where you want to pass some parameter to the test plan or want to share some object or data that is available for all threads to use. In such scenarios, you can use JMeter properties.

    JMeter properties is a map of keys and values, that is accessible to all threads. To access them you can use \${__P(PROPERTY_NAME)} or equivalent \${__property(PROPERTY_NAME) inside almost any string, props['PROPERTY_NAME'] inside groovy scripts or props.get("PROPERTY_NAME") in lambda expressions.

    To set them, you can use prop() method included in EmbeddedJmeterEngine like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.engines.EmbeddedJmeterEngine;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testProperties() {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("http://myservice.test/\${__P(MY_PROP)}")
    +        )
    +    ).runIn(new EmbeddedJmeterEngine()
    +        .prop("MY_PROP", "MY_VAL"));
    +  }
    +
    +}
    +

    Or you can set them in groovy or java code, like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testProperties() {
    +    testPlan(
    +        threadGroup(1, 1,
    +            jsr223Sampler("props.put('MY_PROP', 'MY_VAL')"),
    +            httpSampler("http://myservice.test/\${__P(MY_PROP)}")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Or you can even load them from a file, which might be handy to have different files with different values for different execution profiles (eg: different environments). Eg:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.engines.EmbeddedJmeterEngine;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testProperties() {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("http://myservice.test/\${__P(MY_PROP)}")
    +        )
    +    ).runIn(new EmbeddedJmeterEngine()
    +        .propertiesFile("my.properties"));
    +  }
    +
    +}
    +

    TIP

    You can put any object (not just strings) in properties, but only strings can be accessed via \${__P(PROPERTY_NAME)} and \${__property(PROPERTY_NAME)}.

    Being able to put any kind of object allows you to do very powerful stuff, like implementing a custom cache, or injecting some custom logic to a test plan.

    TIP

    You can also specify properties through JVM system properties either by setting JVM parameter -D or using System.setProperty() method.

    When properties are set as JVM system properties, they are not accessible via props[PROPERTY_NAME] or props.get("PROPERTY_NAME"). If you need to access them from groovy or java code, then use props.getProperty("PROPERTY_NAME") instead.

    WARNING

    JMeter properties can currently only be used with EmbeddedJmeterEngine, so use them sparingly and prefer other mechanisms when available.

    Test resources

    When working with tests in maven projects, even gradle in some scenarios, it is usually necessary to use files hosted in src/test/resources. For example CSV files for csvDataSet, a file to be used by an httpSampler, some JSON for comparison, etc. The DSL provides testResource as a handy shortcut for such scenarios. Here is a simple example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testProperties() throws IOException {
    +    testPlan(
    +        csvDataSet(testResource("users.csv")), // gets users info from src/test/resources/users.csv
    +        threadGroup(1, 1,
    +            httpSampler("http://myservice.test/users/\${USER_ID}")
    +        )
    +    ).run();
    +  }
    +
    +}
    +
    `,15),Fp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/util/TestResource.java",target:"_blank",rel:"noopener noreferrer"},Vp=n("h2",{id:"protocols",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#protocols","aria-hidden":"true"},"#"),s(" Protocols")],-1),Yp=n("h3",{id:"http",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#http","aria-hidden":"true"},"#"),s(" HTTP")],-1),$p=n("p",null,"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.",-1),Xp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/JmeterDsl.java",target:"_blank",rel:"noopener noreferrer"},Kp={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/http/DslHttpSampler.java",target:"_blank",rel:"noopener noreferrer"},Qp=e(`

    Methods & body

    As previously seen, you can do simple gets and posts like in the following snippet:

    httpSampler("http://my.service") // A simple get
    +httpSampler("http://my.service")
    +    .post("{\\"field\\":\\"val\\"}", Type.APPLICATION_JSON) // simple post
    +

    But you can also use additional methods to specify any HTTP method and body:

    httpSampler("http://my.service")
    +  .method(HTTPConstants.PUT)
    +  .contentType(Type.APPLICATION_JSON)
    +  .body("{\\"field\\":\\"val\\"}")
    +

    Additionally, when in need to generate dynamic URLs or bodies, you can use lambda expressions (as previously seen in some examples):

    httpSampler("http://my.service")
    +  .post(s -> buildRequestBody(s.vars), Type.TEXT_PLAIN)
    +httpSampler("http://my.service")
    +  .body(s -> buildRequestBody(s.vars))
    +httpSampler(s -> buildRequestUrl(s.vars)) // buildRequestUrl is just an example of a custom method you could implement with your own logic
    +

    WARNING

    As previously mentioned, even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    Parameters

    In many cases, you will need to specify some URL query string parameters or URL encoded form bodies. For these cases, you can use param method as in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    String 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(HTTPConstants.POST) // POST 
    +                .param("name", "wooden chair")
    +            )
    +    ).run();
    +  }
    +
    +}
    +

    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 rawParam method instead which does not apply any encoding to the parameter name or value, and send it as is.

    Headers

    You might have already noticed in some of the examples that we have shown already some ways to set some headers. For instance, in the following snippet Content-Type header is being set in two different ways:

    httpSampler("http://my.service")
    +  .post("{\\"field\\":\\"val\\"}", Type.APPLICATION_JSON)
    +httpSampler("http://my.service")
    +  .contentType(Type.APPLICATION_JSON)
    +

    These are handy methods to specify the 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")
    +

    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 httpHeaders like this:

    testPlan(
    +    threadGroup(2, 10,
    +        httpHeaders()
    +          .header("X-Header", "val1"),
    +        httpSampler("http://my.service"),
    +        httpSampler("http://my.service/users")
    +    )
    +).run();
    +

    TIP

    You can also use lambda expressions for dynamically building HTTP Headers, but the same limitations apply as in other cases (running in BlazeMeter, OctoPerf, Azure, or using generated JMX file).

    Authentication

    When in need to authenticate user associated to an HTTP request you can either use httpAuth or custom logic (with HTTP headers, regex extractors, variables, and other potential elements) to properly generate the required requests.

    httpAuth greatly simplifies common scenarios like this example using basic auth:

    String baseUrl = "http://my.service";
    +testPlan(
    +    httpAuth()
    +        .basicAuth(baseUrl, System.getenv("AUTH_USER"), System.getenv("AUTH_PASSWORD")),
    +    threadGroup(2, 10,
    +        httpSampler(baseUrl + "/login"),
    +        httpSampler(baseUrl + "/users")
    +    )
    +).run();
    +

    TIP

    Even though you can specify an empty base URL to match any potential request, don't do it. Defining a non-specific enough base URL, may leak credentials to unexpected sites, for example, when used in combination with downloadEmbeddedResources().

    TIP

    Avoid including credentials in repository where code is hosted, which might lead to security leaks.

    In provided example credentials are obtained from environment variable that have to be predefined by user when running tests, but you can also use other approaches to avoid security leaks.

    Also take into consideration that if you use jtlWriter and chose to store HTTP request headers and/or bodies, then JTL could include used credentials and might be also a potential source for security leaks.

    TIP

    Http Authorization Manager, the element used by httpAuth, automatically adds the Authorization header for each request that starts with the given base url. If you need more control (e.g.: only send the header in the first request or under certain condition), you might add httpAuth only to specific requests or just build custom logic through usage of httpHeaders, regexExtractor and jsr223PreProcessor.

    `,27),Zp={class:"custom-container tip"},no=n("p",{class:"custom-container-title"},"TIP",-1),so=n("code",null,"httpAuth()",-1),ao=n("code",null,"basicAuth",-1),to={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},eo={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/http/DslAuthManager.java",target:"_blank",rel:"noopener noreferrer"},po=e(`

    Multipart requests

    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 bodyPart and bodyFilePart methods like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.apache.http.entity.ContentType;
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("https://myservice.com/report")
    +                .method(HTTPConstants.POST)
    +                .bodyPart("myText", "Hello World", ContentType.TEXT_PLAIN)
    +                .bodyFilePart("myFile", "myReport.xml", ContentType.TEXT_XML)
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Cookies & caching

    jmeter-java-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:

    testPlan(
    +    httpCookies().disable(),
    +    httpCache().disable(),
    +    threadGroup(2, 10,
    +        httpSampler("http://my.service")
    +    )
    +)
    +

    Timeouts

    By default, JMeter uses system default configurations for connection and response timeouts (maximum time for a connection to be established or a server response after a request, before it fails). This is might make the test behave different depending on the machine where it runs. To avoid this, it is recommended to always set these values. Here is an example:

    testPlan(
    +    httpDefaults()
    +        .connectionTimeout(Duration.ofSeconds(10))
    +        .responseTimeout(Duration.ofMinutes(1)),
    +    threadGroup(2, 10,
    +        httpSampler("http://my.service")
    +    )
    +)
    +

    WARNING

    Currently we are using same defaults as JMeter to avoid breaking existing test plans executions, but in a future major version we plan to change default setting to avoid the common pitfall previously mentioned.

    Connections

    jmeter-java-dsl, as JMeter (and also K6), by default reuses HTTP connections between thread iterations to avoid common issues with port and file descriptors exhaustion which require manual OS tuning and may manifest in many ways.

    This decision implies that the load generated from 10 threads and 100 iterations is not the same as the one generated by 1000 real users with up to 10 concurrent users in a given time, since the load imposed by each user connection and disconnection would only be generated once for each thread.

    If you need for each iteration to reset connections you can use something like this:

    httpDefaults()
    +    .resetConnectionsBetweenIterations()
    +
    `,15),oo={href:"https://medium.com/@chientranthien/how-to-generate-high-load-benchmark-with-jmeter-80e828a67592",target:"_blank",rel:"noopener noreferrer"},co=e(`

    TIP

    Connections are configured by default with a TTL (time-to-live) of 1 minute, which you can easily change like this:

    httpDefaults()
    +    .connectionTtl(Duration.ofMinutes(10))
    +
    • This and resetConnectionsBetweenIterations apply at the JVM level (due to JMeter limitation), so they affect all requests in the test plan and other ones potentially running in the same JVM instance.

    WARNING

    Using clientImpl(HttpClientImpl.JAVA) will ignore any of the previous settings and will reuse connections depending on JVM implementation.

    Embedded resources

    Sometimes you may need to reproduce a browser behavior, downloading for a given URL all associated resources (images, frames, etc.).

    jmeter-java-dsl allows you to easily reproduce this scenario by using the downloadEmbeddedResources method in httpSampler like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(5, 10,
    +            httpSampler("http://my.service/")
    +                .downloadEmbeddedResources()
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    This will make JMeter automatically parse the HTTP response for embedded resources, download them and register embedded resources downloads as sub-samples of the main sample.

    `,7),io={href:"https://jmeter.apache.org/usermanual/component_reference.html#HTTP_Request",target:"_blank",rel:"noopener noreferrer"},lo=e(`

    TIP

    You can use downloadEmbeddedResourcesNotMatching(urlRegex) and downloadEmbeddedResourcesMatching(urlRegex) methods if you need to ignore, or only download, some embedded resources requests. For example, when some requests are not related to the system under test.

    WARNING

    The DSL, unlike JMeter, uses by default concurrent download of embedded resources (with up to 6 parallel downloads), which is the most used scenario to emulate browser behavior.

    WARNING

    Using downloadEmbeddedResources doesn't allow to download all resources that a browser could download, since it does not execute any JavaScript. For instance, resources URLs solved through JavaScript or direct JavaScript requests will not be requested. Even with this limitation, in many cases just downloading "static" resources is a good enough solution for performance testing.

    Redirects

    When jmeter-java-dsl (using JMeter logic) detects a redirection, it will automatically do a request to the redirected URL and register the redirection as a sub-sample of the main request.

    If you want to disable such logic, you can just call .followRedirects(false) in a given httpSampler.

    HTTP defaults

    Whenever you need to use some repetitive value or common setting among HTTP samplers (and any part of the test plan) the preferred way (due to readability, debugability, traceability, and in some cases simplicity) is to create a Java variable or custom builder method.

    For example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.http.DslHttpSampler;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void performanceTest() throws IOException {
    +    String host = "myservice.my";
    +    testPlan(
    +        threadGroup(10, 100,
    +            productCreatorSampler(host, "Rubber"),
    +            productCreatorSampler(host, "Pencil")
    +        )
    +    ).run();
    +  }
    +
    +  private DslHttpSampler productCreatorSampler(String host, String productName) {
    +    return httpSampler("https://" + host + "/api/product")
    +        .post("{\\"name\\": \\"" + productName + "\\"}", ContentType.APPLICATION_JSON);
    +  }
    +
    +}
    +

    In some cases though, it might be simpler to just use provided httpDefaults method, like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void performanceTest() throws IOException {
    +    testPlan(
    +        httpDefaults()
    +            .url("https://myservice.my")
    +            .downloadEmbeddedResources(),
    +        threadGroup(10, 100,
    +            httpSampler("/products"),
    +            httpSampler("/cart")
    +        )
    +    ).run();
    +  }
    +
    +}
    +
    `,12),uo={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/http/DslHttpDefaults.java",target:"_blank",rel:"noopener noreferrer"},ro=e(`

    Overriding URL protocol, host or port

    In some cases, you might want to use a default base URL but some particular requests may require some part of the URL to be different (eg: protocol, host, or port).

    The preferred way (due to maintainability, language & IDE provided features, traceability, etc) of doing this, as with defaults, is using java code. Eg:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    String protocol = "https://";
    +    String host = "myservice.com";
    +    String baseUrl = protocol + host;
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler(baseUrl + "/products"),
    +            httpSampler(protocol + "api." + host + "/cart"),
    +            httpSampler(baseUrl + "/stores")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    But in some cases, this might be too verbose, or unnatural for users with existing JMeter knowledge. In such cases you can use provided methods (protocol, host & port) to just specify the part you want to modify for the sampler like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpDefaults()
    +                .url("https://myservice.com"),
    +            httpSampler("/products"),
    +            httpSampler("/cart")
    +                .host("subDomain.myservice.com"),
    +            httpSampler("/stores")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Proxy

    `,7),ko={href:"https://www.telerik.com/fiddler",target:"_blank",rel:"noopener noreferrer"},mo={href:"https://mitmproxy.org/",target:"_blank",rel:"noopener noreferrer"},vo=n("code",null,"proxy",-1),ho=e(`
    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("https://myservice.com")
    +                .proxy("http://myproxy:8081")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    TIP

    You can also specify proxy authentication parameters with proxy(url, username, password) method.

    TIP

    When you need to set a proxy for several samplers, use httpDefaults().proxy methods.

    GraphQL

    When you want to test a GraphQL service, having properly set each field in an HTTP request and knowing the exact syntax for each of them, can quickly start becoming tedious. For this purpose, jmeter-java-dsl provides graphqlSampler. To use it you need to include this dependency:

    `,5),bo=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-graphql"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),go=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-graphql:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),fo=e(`

    And then you can make simple GraphQL requests like this:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.graphql.DslGraphqlSampler.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    String url = "https://myservice.com";
    +    testPlan(
    +        threadGroup(1, 1,
    +            graphqlSampler(url, "{user(id: 1) {name}}"),
    +            graphqlSampler(url, "query UserQuery($id: Int) { user(id: $id) {name}}")
    +                .operationName("UserQuery")
    +                .variable("id", 2)
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    TIP

    GraphQL Sampler is based on HTTP Sampler, so all test elements that affect HTTP Samplers, like httpHeaders, httpCookies, httpDefaults, and JMeter properties, also affect GraphQL sampler.

    WARNING

    grapqlSampler sets by default application/json Content-Type header.

    This has been done to ease the most common use cases and to avoid users the common pitfall of missing the proper Content-Type header value.

    If you need to modify graphqlSampler content type to be other than application/json, then you can use contentType method, potentially parameterizing it to reuse the same value in multiple samplers like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.graphql.DslGraphqlSampler.*;
    +
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.graphql.DslGraphqlSampler;
    +
    +public class PerformanceTest {
    +
    +  private DslGraphqlSampler myGraphqlRequest(String query) {
    +    return graphqlSampler("https://myservice.com", query)
    +        .contentType(ContentType.create("myContentType"));
    +  }
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            myGraphqlRequest("{user(id: 1) {name}}"),
    +            myGraphqlRequest("{user(id: 5) {address}}")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    JDBC and databases interactions

    Several times you will need to interact with a database to either set it to a known state while setting up the test plan, clean it up while tearing down the test plan, or even check or generate some values in the database while the test plan is running.

    For these use cases, you can use JDBC DSL-provided elements.

    Including the following dependency in your project:

    `,8),yo=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-jdbc"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),wo=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-jdbc:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),jo=n("p",null,"And adding a proper JDBC driver for your database, like this example for PostgreSQL:",-1),_o=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("org.postgresql"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("postgresql"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("42.3.1"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),qo=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'org.postgresql:postgresql:42.3.1'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),To=e(`

    You can interact with the database like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.jdbc.JdbcJmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.sql.Types;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import org.postgresql.Driver;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +import us.abstracta.jmeter.javadsl.jdbc.DslJdbcSampler;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    String jdbcPoolName = "pgLocalPool";
    +    String productName = "dsltest-prod";
    +    DslJdbcSampler cleanUpSampler = jdbcSampler(jdbcPoolName,
    +        "DELETE FROM products WHERE name = '" + productName + "'")
    +        .timeout(Duration.ofSeconds(10));
    +    TestPlanStats stats = testPlan(
    +        jdbcConnectionPool(jdbcPoolName, Driver.class, "jdbc:postgresql://localhost/my_db")
    +            .user("user")
    +            .password("pass"),
    +        setupThreadGroup(
    +            cleanUpSampler
    +        ),
    +        threadGroup(5, 10,
    +            httpSampler("CreateProduct", "http://my.service/products")
    +                .post("{\\"name\\", \\"" + productName + "\\"}", ContentType.APPLICATION_JSON),
    +            jdbcSampler("GetProductsIdsByName", jdbcPoolName,
    +                "SELECT id FROM products WHERE name=?")
    +                .param(productName, Types.VARCHAR)
    +                .vars("PRODUCT_ID")
    +                .timeout(Duration.ofSeconds(10)),
    +            httpSampler("GetLatestProduct",
    +                "http://my.service/products/\${__V(PRODUCT_ID_\${PRODUCT_ID_#})}")
    +        ),
    +        teardownThreadGroup(
    +            cleanUpSampler
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    Always specify a query timeout to quickly identify unexpected behaviors in queries.

    TIP

    Don't forget proper WHERE conditions in UPDATES and DELETES, and proper indexes for table columns participating in WHERE conditions 😊.

    `,4),xo={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-jdbc/src/main/java/us/abstracta/jmeter/javadsl/jdbc/JdbcJmeterDsl.java",target:"_blank",rel:"noopener noreferrer"},Po={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-jdbc/src/test/java/us/abstracta/jmeter/javadsl/jdbc/JdbcJmeterDslTest.java",target:"_blank",rel:"noopener noreferrer"},So=e(`

    Java API performance testing

    Sometimes JMeter provided samplers are not enough for testing a particular technology, custom code, or service that requires some custom code to interact with. For these cases, you might use jsr223Sampler which allows you to use custom logic to generate a sample result.

    Here is an example for load testing a Redis server:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class TestRedis {
    +
    +  @Test
    +  public void shouldGetExpectedSampleResultWhenJsr223SamplerWithLambdaAndCustomResponse()
    +      throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            jsr223Sampler("import redis.clients.jedis.Jedis\\n"
    +                + "Jedis jedis = new Jedis('localhost', 6379)\\n"
    +                + "jedis.connect()\\n"
    +                + "SampleResult.connectEnd()\\n"
    +                + "jedis.set('foo', 'bar')\\n"
    +                + "return jedis.get(\\"foo\\")")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofMillis(500));
    +  }
    +
    +}
    +
    `,4),Io={class:"custom-container tip"},Do=n("p",{class:"custom-container-title"},"TIP",-1),Eo=n("p",null,"Remember to add any particular dependencies required by your code. For example, the above example requires this dependency:",-1),Jo=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("redis.clients"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jedis"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("3.6.0"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),Co=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'redis.clients:jedis:3.6.0'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),Ao=e(`

    You can also use Java lambdas instead of Groovy script to take advantage of IDEs auto-completion, Java type safety, and less CPU consumption:

    jsr223Sampler(v -> {
    +    SampleResult result = v.sampleResult;
    +    Jedis jedis = new Jedis("localhost", 6379);
    +    jedis.connect();
    +    result.connectEnd();
    +    jedis.set("foo", "bar");
    +    result.setResponseData(jedis.get("foo"), StandardCharsets.UTF_8.name());
    +})
    +

    WARNING

    As previously mentioned, even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    You may even use some custom logic that executes a particular logic when a thread group thread is created and finished. Here is an example:

    public class TestRedis {
    +
    +  public static class RedisSampler implements SamplerScript, ThreadListener {
    +
    +    private Jedis jedis;
    +
    +    @Override
    +    public void threadStarted() {
    +      jedis = new Jedis("localhost", 6379);
    +      jedis.connect();
    +    }
    +
    +    @Override
    +    public void runScript(SamplerVars v) {
    +      jedis.set("foo", "bar");
    +      v.sampleResult.setResponseData(jedis.get("foo"), StandardCharsets.UTF_8.name());
    +    }
    +
    +    @Override
    +    public void threadFinished() {
    +      jedis.close();
    +    }
    +
    +  }
    +
    +  @Test
    +  public void shouldGetExpectedSampleResultWhenJsr223SamplerWithLambdaAndCustomResponse()
    +      throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            jsr223Sampler(RedisSampler.class)
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofMillis(500));
    +  }
    +
    +}
    +

    TIP

    You can also make your class implement TestIterationListener to execute custom logic on each thread group iteration start, or LoopIterationListener to execute some custom logic on each iteration start (for example, each iteration of a forLoop).

    TIP

    When using public static classes in jsr223Sampler take into consideration that one instance of the class is created for each thread group thread and jsr223Sampler instance.

    Note: jsr223Sampler is very powerful, but also makes code and test plans harder to maintain (as with any custom code) compared to using JMeter built-in samplers. So, in general, prefer using JMeter-provided samplers if they are enough for the task at hand, and use jsr223Sampler sparingly.

    `,8),Mo={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/java/DslJsr223Sampler.java",target:"_blank",rel:"noopener noreferrer"},Oo=e(`

    Selenium

    With JMeter DSL is quite simple to integrate your existing selenium scripts into performance tests. One common use case is to do real user monitoring or synthetics monitoring (get time spent in particular parts of a Selenium script) while the backend load is being generated.

    Here is an example of how you can do this with JMeter DSL:

    public class PerformanceTest {
    +
    +  public static class SeleniumSampler implements SamplerScript, ThreadListener {
    +
    +    private WebDriver driver;
    +
    +    @Override
    +    public void threadStarted() {
    +      driver = new ChromeDriver(); // you can invoke existing set up logic to reuse it
    +    }
    +
    +    @Override
    +    public void runScript(SamplerVars v) {
    +      driver.get("https://mysite"); // you can invoke existing selenium script for reuse here
    +    }
    +
    +    @Override
    +    public void threadFinished() {
    +      driver.close(); // you can invoke existing tear down logic to reuse it
    +    }
    +
    +  }
    +
    +  @Test
    +  public void shouldGetExpectedSampleResultWhenJsr223SamplerWithLambdaAndCustomResponse()
    +      throws IOException {
    +    Duration testPlanDuration = Duration.ofMinutes(10);
    +    TestPlanStats stats = testPlan(
    +        threadGroup(1, testPlanDuration,
    +            jsr223Sampler("Real User Monitor", SeleniumSampler.class)
    +        ),
    +        threadGroup(100, testPlanDuration,
    +            httpSampler("https://mysite/products")
    +                .post("{\\"name\\": \\"test\\"}", Type.APPLICATION_JSON)
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofMillis(500));
    +  }
    +
    +}
    +

    Check previous section for more details on jsr223Sampler.

    Custom or yet not supported test elements

    `,6),Lo={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},Ro={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/CONTRIBUTING.md",target:"_blank",rel:"noopener noreferrer"},Go=e(`

    In some cases though, you might have some private custom test element that you don't want to publish or share with the rest of the community, or you are just really in a hurry and want to use it while the proper support is included in the DSL.

    For such cases, the preferred approach is implementing a builder class for the test element. Eg:

    import org.apache.jmeter.testelement.TestElement;
    +import us.abstracta.jmeter.javadsl.core.samplers.BaseSampler;
    +
    +public class DslCustomSampler extends BaseSampler<DslCustomSampler> {
    +
    +  private String myProp;
    +
    +  private DslCustomSampler(String name) {
    +    super(name, CustomSamplerGui.class); // you can pass null here if custom sampler is a test bean
    +  }
    +
    +  public DslCustomSampler myProp(String val) {
    +    this.myProp = val;
    +    return this;
    +  }
    +
    +  @Override
    +  protected TestElement buildTestElement() {
    +    CustomSampler ret = new CustomSampler();
    +    ret.setMyProp(myProp);
    +    return ret;
    +  }
    +
    +  public static DslCustomSampler customSampler(String name) {
    +    return new DslCustomSampler(name);
    +  }
    +
    +}
    +

    Which you can use as any other JMeter DSL component, like in this example:

    import static us.abstracta.jmeter.javadsl.DslCustomSampler.*;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            customSampler("mySampler")
    +                .myProp("myVal")
    +            )
    +    ).run();
    +  }
    +
    +}
    +

    This approach allows for easy reuse, compact and simple usage in tests, and you might even create your own CustomJmeterDsl class containing builder methods for many custom components.

    Alternatively, when you want to skip creating subclasses, you might use the DSL wrapper module.

    Include the module on your project:

    `,8),No=n("div",{class:"language-xml line-numbers-mode","data-ext":"xml"},[n("pre",{class:"language-xml"},[n("code",null,[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("dependency")]),n("span",{class:"token punctuation"},">")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("groupId")]),n("span",{class:"token punctuation"},">")]),s("us.abstracta.jmeter"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("artifactId")]),n("span",{class:"token punctuation"},">")]),s("jmeter-java-dsl-wrapper"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("version")]),n("span",{class:"token punctuation"},">")]),s("1.29"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("scope")]),n("span",{class:"token punctuation"},">")]),s("test"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),zo=n("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[n("pre",{class:"language-groovy"},[n("code",null,[s("testImplementation "),n("span",{class:"token string"},"'us.abstracta.jmeter:jmeter-java-dsl-wrapper:1.29'"),s(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"})])],-1),Uo=e(`

    And use a wrapper like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.wrapper.WrapperJmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            testElement("mySampler", new CustomSamplerGui()) // for test beans you can just provide the test bean instance
    +                .prop("myProp","myVal")
    +            )
    +    ).run();
    +  }
    +
    +}
    +
    `,2),Bo={href:"https://github.com/abstracta/jmeter-java-dsl/tree/master/jmeter-java-dsl-wrapper/src/main/java/us/abstracta/jmeter/javadsl/wrapper/WrapperJmeterDsl.java",target:"_blank",rel:"noopener noreferrer"},Ho=e(`

    JMX support

    Save as JMX

    In case you want to load a test plan in JMeter GUI, you can save it just invoking saveAsJMX method in the test plan as in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +public class SaveTestPlanAsJMX {
    +
    +  public static void main(String[] args) throws Exception {
    +    testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        )
    +    ).saveAsJmx("dsl-test-plan.jmx");
    +  }
    +
    +}
    +
    `,4),Wo={href:"https://github.com/abstracta/jmeter-java-dsl/issues",target:"_blank",rel:"noopener noreferrer"},Fo=e(`

    TIP

    If you get any error (like CannotResolveClassException) while loading the JMX in JMeter GUI, you can try copying jmeter-java-dsl jar (and any other potential modules you use) to JMeter lib directory, restart JMeter and try loading the JMX again.

    TIP

    If you want to migrate changes done in JMX to the Java DSL, you can use jmx2dsl as an accelerator. The resulting plan might differ from the original one, so sometimes it makes sense to use it, and some it is faster just to port the changes manually.

    WARNING

    If you use JSR223 Pre- or Post-processors with Java code (lambdas) instead of strings or use one of the HTTP Sampler methods which receive a function as a parameter, then the exported JMX will not work in JMeter GUI. You can migrate them to use jsrPreProcessor with string scripts instead.

    Run JMX file

    jmeter-java-dsl also provides means to easily run a test plan from a JMX file either locally, in BlazeMeter (through previously mentioned jmeter-java-dsl-blazemeter module), OctoPerf (through jmeter-java-dsl-octoperf module), or Azure Load testing (through jmeter-java-dsl-azure module). Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.DslTestPlan;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class RunJmxTestPlan {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = DslTestPlan.fromJmx("test-plan.jmx").run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    This can be used to just run existing JMX files, or when DSL has no support for some JMeter functionality or plugin (although you can use wrappers for this), and you need to use JMeter GUI to build the test plan but still want to use jmeter-java-dsl to run the test plan embedded in Java test or code.

    TIP

    When the JMX uses some custom plugin or JMeter protocol support, you might need to add required dependencies to be able to run the test in an embedded engine. For example, when running a TN3270 JMX test plan using RTE plugin you will need to add the following repository and dependencies:

    <repositories>
    +  <repository>
    +    <id>jitpack.io</id>
    +    <url>https://jitpack.io</url>
    +  </repository>
    +</repositories>
    +
    +<dependencies>
    +  ...
    +  <dependency>
    +    <groupId>com.github.Blazemeter</groupId>
    +    <artifactId>RTEPlugin</artifactId>
    +    <version>3.1</version>
    +    <scope>test</scope>
    +  </dependency>
    +  <dependency>
    +    <groupId>com.github.Blazemeter</groupId>
    +    <artifactId>dm3270</artifactId>
    +    <version>0.12.3-lib</version>
    +    <scope>test</scope>
    +  </dependency>
    +</dependencies>
    +
    `,8);function Vo(Yo,$o){const t=i("ExternalLinkIcon"),o=i("CodeGroupItem"),c=i("CodeGroup");return u(),r("div",null,[D,E,n("p",null,[s("Provided examples use "),n("a",J,[s("JUnit 5"),a(t)]),s(" and "),n("a",C,[s("AssertJ"),a(t)]),s(", but you can use other test & assertion libraries.")]),n("p",null,[s("Explore the DSL in your preferred IDE to discover all available features, and consider reviewing "),n("a",A,[s("existing tests"),a(t)]),s(" for additional examples.")]),n("p",null,[s("The DSL currently supports most common used cases, keeping it simple and avoiding investing development effort in features that might not be needed. If you identify any particular scenario (or JMeter feature) that you need and is not currently supported, or easy to use, "),n("strong",null,[s("please let us know by "),n("a",M,[s("creating an issue"),a(t)])]),s(" and we will try to implement it as soon as possible. Usually porting JMeter features is quite fast.")]),n("div",O,[L,n("p",null,[s("If you like this project, "),n("strong",null,[s("please give it a star ⭐ in "),n("a",R,[s("GitHub"),a(t)]),s("!")]),s(" This helps the project be more visible, gain relevance, and encourages us to invest more effort in new features.")])]),n("p",null,[s("For an intro to JMeter concepts and components, you can check "),n("a",G,[s("JMeter official documentation"),a(t)]),s(".")]),N,z,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[U]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[B]),_:1})]),_:1}),n("div",H,[W,n("p",null,[n("a",F,[s("Here"),a(t)]),s(" is a sample project in case you want to start one from scratch.")])]),V,n("div",Y,[$,n("p",null,[s("Since JMeter uses "),n("a",X,[s("log4j2"),a(t)]),s(", if you want to control the logging level or output, you can use something similar to this "),n("a",K,[s("log4j2.xml"),a(t)]),s(".")])]),n("div",Q,[Z,n("p",null,[s("Keep in mind that you can use Java programming to modularize and create abstractions which allow you to build complex test plans that are still easy to read, use and maintain. "),n("a",nn,[s("Here is an example"),a(t)]),s(" of some complex abstraction built using Java features and the DSL.")])]),sn,an,tn,en,pn,n("div",on,[cn,n("p",null,[s("You can use "),n("a",ln,[s("jbang"),a(t)]),s(" to easily execute the recorder with the latest version available. E.g.:")]),un]),rn,n("p",null,[s("To avoid fragile test plans with fixed values in request parameters, the DSL recorder, through the usage of the "),n("a",kn,[s("JMeter Correlation Recorder Plugin"),a(t)]),s(", allows you to define correlation rules.")]),dn,n("div",mn,[vn,hn,n("p",null,[s("We have ideas to ease this for the future, but, if you have ideas, or just want to give more priority to improving this, please create an "),n("a",bn,[s("issue in the repository"),a(t)]),s(" to let us know.")])]),gn,n("p",null,[s("To ease migrating existing JMeter test plans and ease learning about DSL features, the DSL provides "),fn,s(" cli command (download the latest cli version from "),n("a",yn,[s("releases page"),a(t)]),s(" or use "),n("a",wn,[s("jbang"),a(t)]),s(") command line tool which you can use to generate DSL code from existing JMX files.")]),jn,a(c,null,{default:p(()=>[a(o,{title:"Java"},{default:p(()=>[_n]),_:1}),a(o,{title:"Jbang"},{default:p(()=>[qn]),_:1})]),_:1}),Tn,n("div",xn,[Pn,Sn,n("p",null,[s("If you find any potential improvement to code generation, "),n("strong",null,[s("please help us by creating an "),n("a",In,[s("issue"),a(t)]),s(" or "),n("a",Dn,[s("discussion"),a(t)])]),s(" in GitHub repository.")])]),En,Jn,Cn,An,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[Mn]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[On]),_:1})]),_:1}),n("p",null,[s("You can easily run a JMeter test plan at scale in "),n("a",Ln,[s("BlazeMeter"),a(t)]),s(" like this:")]),Rn,n("p",null,[s("Note that is as simple as "),n("a",Gn,[s("generating a BlazeMeter authentication token"),a(t)]),s(" and adding "),Nn,s(" to any existing jmeter-java-dsl test to get it running at scale in BlazeMeter.")]),zn,Un,n("p",null,[s("Check "),n("a",Bn,[s("BlazeMeterEngine"),a(t)]),s(" for details on usage and available settings when running tests in BlazeMeter.")]),Hn,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[Wn]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[Fn]),_:1})]),_:1}),n("p",null,[s("You can easily run a JMeter test plan at scale in "),n("a",Vn,[s("OctoPerf"),a(t)]),s(" like this:")]),Yn,n("p",null,[s("Note that, as with the BlazeMeter case, it is as simple as "),n("a",$n,[s("getting the OctoPerf API key"),a(t)]),s(" and adding "),Xn,s(" to any existing jmeter-java-dsl test to get it running at scale in OctoPerf.")]),Kn,Qn,n("p",null,[s("Check "),n("a",Zn,[s("OctoPerfEngine"),a(t)]),s(" for details on usage and available settings when running tests in OctoPerf.")]),ns,n("div",ss,[as,n("p",null,[s("There is currently no built-in support for test elements with Java lambdas in "),ts,s(" (as there is for "),es,s("). If you need it, please request it by creating a "),n("a",ps,[s("GitHub issue"),a(t)]),s(".")])]),os,cs,n("p",null,[s("To use "),n("a",is,[s("Azure Load Testing"),a(t)]),s(" to execute your test plans at scale, is as easy as including the following module as a dependency:")]),a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[ls]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[us]),_:1})]),_:1}),rs,n("blockquote",null,[n("p",null,[s("This test is using "),ks,s(", a custom environment variable containing "),ds,s(" with proper values for each. Check in "),n("a",ms,[s("Azure Portal tenant properties"),a(t)]),s(" the proper tenant ID for your subscription, and follow "),n("a",vs,[s("this guide"),a(t)]),s(" to register an application with proper permissions and secrets generation for tests execution.")])]),hs,bs,n("p",null,[s("Check "),n("a",gs,[s("AzureEngine"),a(t)]),s(" for details on usage and available settings when running tests in Azure Load Testing.")]),fs,n("div",ys,[ws,n("p",null,[s("There is currently no built-in support for test elements with Java lambdas in "),js,s(" (as there is for "),_s,s("). If you need it, please request it by creating a "),n("a",qs,[s("GitHub issue"),a(t)]),s(".")])]),Ts,xs,n("p",null,[s("JMeter already provides means to run a test on several machines controlled by one master/client machine. This is referred as "),n("a",Ps,[s("Remote Testing"),a(t)]),s(".")]),Ss,n("div",Is,[Ds,n("p",null,[s("Or check JMeter DSL "),n("a",Es,[s("pom.xml property "),Js,a(t)]),s(".")])]),Cs,As,n("div",Ms,[Os,n("p",null,[n("a",Ls,[s("Here"),a(t)]),s(" is an example project using "),Rs,s(" that starts a JMeter server/slave and executes a test with it. If you want to do a similar setup, generate your own keystore and properly tune RMI remote server in server/slave.")])]),n("p",null,[s("Check "),n("a",Gs,[s("DistributedJmeterEngine"),a(t)]),s(" and "),n("a",Ns,[s("JMeter documentation"),a(t)]),s(" for proper setup and additional options.")]),zs,n("p",null,[s("Check "),n("a",Us,[s("AutoStopListener"),a(t)]),s(" for details on available options for auto-stop conditions.")]),n("p",null,[Bs,s(" is inspired in "),n("a",Hs,[s("JMeter AutoStop Plugin"),a(t)]),s(", but provides a lot more flexibility.")]),Ws,n("div",Fs,[Vs,Ys,n("p",null,[s("This behavior is different from "),n("a",$s,[s("JMeter AutoStop Plugin"),a(t)]),s(", which evaluates and resets aggregations (it only provides average aggregation) for every second.")]),Xs]),Ks,n("div",Qs,[Zs,n("p",null,[na,s(" will automatically work with "),sa,s(". But no support has been implemented yet for "),aa,s(" or "),ta,s(". If you need such support, please create "),n("a",ea,[s("an issue in the GitHub repository"),a(t)]),s(".")])]),pa,n("p",null,[s("Check "),n("a",oa,[s("DslDefaultThreadGroup"),a(t)]),s(" for more details.")]),ca,n("p",null,[s("This will internally use JMeter "),n("a",ia,[s("Concurrency Thread Group"),a(t)]),s(" element in combination with "),n("a",la,[s("Throughput Shaping Time"),a(t)]),s(".")]),ua,n("p",null,[s("Check "),n("a",ra,[s("RpsThreadGroup"),a(t)]),s(" for more details.")]),ka,n("p",null,[s("Check "),n("a",da,[s("DslSetupThreadGroup"),a(t)]),s(" and "),n("a",ma,[s("DslTeardownThreadGroup"),a(t)]),s(" for additional tips and details on the usage of these components.")]),va,n("div",ha,[ba,n("p",null,[s("DSL provides following methods to ease results and variables visualization and debugging: "),ga,s(", "),fa,s(", "),ya,s(", "),wa,s(", "),ja,s(", "),_a,s(". Check "),n("a",qa,[s("PostProcessorVars"),a(t)]),s(" and "),n("a",Ta,[s("Jsr223ScriptVars"),a(t)]),s(" for more details.")])]),xa,n("div",Pa,[Sa,n("p",null,[s("By default, "),Ia,s(" will only include JMeter variables in generated sub sampler, which covers the most used case and keeps memory and disk usage low. "),Da,s(" includes additional methods that allow including other information like sampler properties, JMeter properties, and system properties. Check "),n("a",Ea,[s("DslDebugPostProcessor"),a(t)]),s(" for more details.")])]),Ja,n("p",null,[s("In some cases, you may want to debug some Groovy script used in some sampler, pre-, or post-processor. For such scenarios, you can check "),n("a",Ca,[s("here"),a(t)]),s(" where we list some options.")]),Aa,n("p",null,[s("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 "),Ma,s(" (which uses "),n("a",Oa,[s("Dummy Sampler plugin"),a(t)]),s(") to be able to test extractors, assertions, controllers conditions, and other parts of the test plan under certain conditions/results generated by the samplers.")]),La,n("p",null,[s("Check "),n("a",Ra,[s("DslDummySampler"),a(t)]),s(" for more information o additional configuration and options.")]),Ga,n("div",Na,[za,n("p",null,[s("By default, "),Ua,s(" 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 "),Ba,s(" with "),Ha,s(" 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 "),Wa,s(" and only log what you need, check "),n("a",Fa,[s("JtlWriter"),a(t)]),s(" for more details.")])]),Va,n("p",null,[s("Check "),n("a",Ya,[s("ResponseFileSaver"),a(t)]),s(" for more details.")]),$a,n("p",null,[s("Check "),n("a",Xa,[s("DslJsr223PostProcessor"),a(t)]),s(" for more details.")]),Ka,Qa,Za,n("p",null,[s("To overcome these limitations you can use provided support for publishing JMeter test run metrics to "),n("a",nt,[s("InfluxDB"),a(t)]),s(" or "),n("a",st,[s("Elasticsearch"),a(t)]),s(", which allows keeping a record of all run statistics and, through "),n("a",at,[s("Grafana"),a(t)]),s(", get some nice dashboards like the following one:")]),tt,n("p",null,[s("This can be easily done using "),et,s(", an existing InfluxDB & Grafana server, and using a dashboard like "),n("a",pt,[s("this one"),a(t)]),s(".")]),ot,n("p",null,[s("If you want to try it locally, you can run "),ct,s(" (previously "),n("a",it,[s("installing Docker"),a(t)]),s(" in your machine) inside "),n("a",lt,[s("this directory"),a(t)]),s(". After containers are started, you can open Grafana at "),n("a",ut,[s("http://localhost:3000"),a(t)]),s(". Finally, run a performance test using the "),rt,s(" and you will be able to see the live results, and keep historic data. Cool, isn't it?!")]),kt,n("p",null,[s("Check "),n("a",dt,[s("InfluxDbBackendListener"),a(t)]),s(" for additional details and settings.")]),mt,n("p",null,[s("As in the InfluxDB scenario, you can try it locally by running "),vt,s(" (previously "),n("a",ht,[s("installing Docker"),a(t)]),s(" in your machine) inside "),n("a",bt,[s("this directory"),a(t)]),s(". After containers are started, you can follow the same steps as in the InfluxDB scenario.")]),gt,n("p",null,[s("Another alternative is using provided "),ft,s(" module with Elasticsearch and Grafana servers using a dashboard like "),n("a",yt,[s("this one"),a(t)]),s(".")]),wt,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[jt]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[_t]),_:1})]),_:1}),qt,n("div",Tt,[xt,n("p",null,[s("This module uses "),n("a",Pt,[s("this JMeter plugin"),a(t)]),s(" which, at its current version, has performance and dependency issues that might affect your project. "),n("a",St,[s("This"),a(t)]),s(" and "),n("a",It,[s("this"),a(t)]),s(" pull requests fix those issues, but until they are merged and released, you might face such issues.")])]),n("p",null,[s("In the same fashion as InfluxDB, if you want to try it locally, you can run "),Dt,s(" inside "),n("a",Et,[s("this directory"),a(t)]),s(" and follow similar steps "),Jt,s(" to visualize live metrics in Grafana.")]),Ct,n("p",null,[s("Check "),n("a",At,[s("ElasticsearchBackendListener"),a(t)]),s(" for additional details and settings.")]),Mt,Ot,Lt,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[Rt]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[Gt]),_:1})]),_:1}),Nt,n("p",null,[s("As in previous cases, you can to try it locally by running "),zt,s(" inside "),n("a",Ut,[s("this directory"),a(t)]),s(". After containers are started, you can follow the same steps as in previous scenarios.")]),Bt,n("p",null,[s("Check "),n("a",Ht,[s("DslPrometheusListener"),a(t)]),s(" for details on listener settings.")]),Wt,n("p",null,[s("Another option is using "),Ft,s(" module which uses existing "),n("a",Vt,[s("jmeter-datadog-backend-listener plugin"),a(t)]),s(" to upload metrics to datadog which you can easily visualize and analize with "),n("a",Yt,[s("DataDog provided JMeter dashboard"),a(t)]),s(". Here is an example of what you get:")]),$t,Xt,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[Kt]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[Qt]),_:1})]),_:1}),Zt,n("div",ne,[se,n("p",null,[s("You can use "),ae,s(" to add additional information to metrics sent to DataDog. Check "),n("a",te,[s("DataDog documentation"),a(t)]),s(" for more details.")])]),ee,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[pe]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[oe]),_:1})]),_:1}),ce,n("p",null,[s("Check "),n("a",ie,[s("Response Assertion"),a(t)]),s(" for more details and additional options.")]),le,n("div",ue,[re,n("p",null,[s("By default this element uses JMeter JSON "),n("a",ke,[s("JMESPath"),a(t)]),s(" Assertion element, and in consequence, JMESPath as query language.")]),n("p",null,[s("If you want to use JMeter JSON Assertion element, and in consequence "),n("a",de,[s("JSONPath"),a(t)]),s(" as the query language, you can simply use "),me,s(" and a JSONPath query.")])]),ve,n("p",null,[s("Check "),n("a",he,[s("DslJsr223PostProcessor"),a(t)]),s(" for more details and additional options.")]),be,n("p",null,[s("As previously mentioned, using Java lambdas is in general more performant than using Groovy scripts ("),n("a",ge,[s("here"),a(t)]),s(" are some comparisons) and are easier to develop and maintain due to type safety, IDE autocompletion, etc.")]),fe,n("ol",null,[ye,n("li",null,[we,n("div",je,[_e,n("p",null,[s("Currently only "),qe,s(" and "),Te,s(" provide a way to upload assets. If you need support for other engines, please request it "),n("a",xe,[s("in an issue"),a(t)]),s(".")])])]),Pe]),Se,n("p",null,[s("Check "),n("a",Ie,[s("DslRegexExtractor"),a(t)]),s(" for more details and additional options.")]),De,n("p",null,[s("Check "),n("a",Ee,[s("DslBoundaryExtractor"),a(t)]),s(" for more details and additional options.")]),Je,n("div",Ce,[Ae,n("p",null,[s("By default this element uses JMeter JSON "),n("a",Me,[s("JMESPath"),a(t)]),s(" Extractor element, and in consequence JMESPath as query language.")]),n("p",null,[s("If you want to use JMeter JSON Extractor element, and in consequence "),n("a",Oe,[s("JSONPath"),a(t)]),s(" as query language, you can simply use "),Le,s(" and a JSONPath query.")])]),Re,n("p",null,[s("Check "),n("a",Ge,[s("DslIfController"),a(t)]),s(" and "),n("a",Ne,[s("JMeter Component documentation"),a(t)]),s(" for more details.")]),ze,n("p",null,[s("Check "),n("a",Ue,[s("DslForEachController"),a(t)]),s(" for more details.")]),Be,n("p",null,[s("If at any time you want to execute a given part of a test plan, inside a thread iteration, while a condition is met, then you can use "),He,s(" (internally using "),n("a",We,[s("JMeter While Controller"),a(t)]),s(") like in the following example:")]),Fe,n("p",null,[s("Check "),n("a",Ve,[s("DslWhileController"),a(t)]),s(" for more details.")]),Ye,n("p",null,[s("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 "),$e,s(" (which uses "),n("a",Xe,[s("JMeter Loop Controller component"),a(t)]),s(") as in the following example:")]),Ke,n("p",null,[s("Check "),n("a",Qe,[s("ForLoopController"),a(t)]),s(" for more details.")]),Ze,n("p",null,[s("Check "),n("a",np,[s("DslRuntimeController"),a(t)]),s(" for more details.")]),sp,n("p",null,[s("In some cases, you only need to run part of a test plan once. For these need, you can use "),ap,s(". This controller will execute a part of the test plan only one time on the first iteration of each thread (using "),n("a",tp,[s("JMeter Once Only Controller Component"),a(t)]),s(").")]),ep,n("p",null,[s("Check "),n("a",pp,[s("DslOnceOnlyController"),a(t)]),s(" for more details.")]),op,n("div",cp,[ip,n("p",null,[s("You can use the "),lp,s(" method to get CSV lines in random order (using "),n("a",up,[s("Random CSV Data Set plugin"),a(t)]),s("), but this is less performant as getting them sequentially, so use it sparingly.")])]),n("p",null,[s("Check "),n("a",rp,[s("DslCsvDataSet"),a(t)]),s(" for additional details and options (like changing delimiter, handling files without headers line, stopping on the end of file, etc.).")]),kp,n("p",null,[s("Check "),n("a",dp,[s("DslCounter"),a(t)]),s(" for more details.")]),mp,n("p",null,[s("Check "),n("a",vp,[s("DslJsr223PreProcessor"),a(t)]),s(" & "),n("a",hp,[s("DslHttpSampler"),a(t)]),s(" for more details and additional options.")]),bp,n("div",gp,[fp,yp,n("p",null,[s("Check "),n("a",wp,[s("DslThroughputTimer"),a(t)]),s(" for more details.")])]),jp,n("p",null,[s("Here is a diagram depicting this behavior, extracted from "),n("a",_p,[s("this nice example"),a(t)]),s(" provided by one of JMeter DSL users:")]),qp,n("p",null,[s("Check "),n("a",Tp,[s("PercentController"),a(t)]),s(" for more details.")]),xp,n("p",null,[n("a",Pp,[s("DslWeightedSwitchController"),a(t)]),s(" for more details.")]),Sp,n("p",null,[s("JMeter provides two main ways for running requests in parallel: thread groups and HTTP samplers downloading embedded resources in parallel. But in some cases is necessary to run requests in parallel which can't be properly modeled with previously mentioned scenarios. For such cases, you can use "),Ip,s(" which allows using the "),n("a",Dp,[s("Parallel Controller plugin"),a(t)]),s(" to execute a given set of requests in parallel (while in a JMeter thread iteration step).")]),Ep,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[Jp]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[Cp]),_:1})]),_:1}),Ap,n("p",null,[s("Check "),n("a",Mp,[s("ParallelController"),a(t)]),s(" for additional info.")]),Op,n("div",Lp,[Rp,Gp,n("p",null,[Np,s(" internally uses JMeter User Defined Variables (aka UDV) when placed as a test plan child, but a JSR223 sampler otherwise. This decision avoids several non-intuitive behaviors of JMeter UDV which are listed in red blocks in "),n("a",zp,[s("the JMeter component documentation"),a(t)]),s(".")]),Up,Bp]),n("p",null,[s("Check "),n("a",Hp,[s("DslVariables"),a(t)]),s(" for more details.")]),Wp,n("p",null,[s("Check "),n("a",Fp,[s("TestResource"),a(t)]),s(" for some further details.")]),Vp,Yp,$p,n("p",null,[s("Here we show some of them, but check "),n("a",Xp,[s("JmeterDsl"),a(t)]),s(" and "),n("a",Kp,[s("DslHttpSampler"),a(t)]),s(" to explore all available features.")]),Qp,n("div",Zp,[no,n("p",null,[s("Currently "),so,s(" only provides "),ao,s(" method. If you need other scenarios, please let us know by creating an "),n("a",to,[s("issue in the repository"),a(t)]),s(".")])]),n("p",null,[s("You can check additional details in "),n("a",eo,[s("DslAuthManager"),a(t)]),s(".")]),po,n("p",null,[s('If you use this setting you might want to take a look at "Config your environment" section of '),n("a",oo,[s("this article"),a(t)]),s(" to avoid port and file descriptors exhaustion.")]),co,n("p",null,[s("Check "),n("a",io,[s("JMeter documentation"),a(t)]),s(" for additional details on downloaded embedded resources.")]),lo,n("p",null,[s("Check "),n("a",uo,[s("DslHttpDefaults"),a(t)]),s(" for additional details on available default options.")]),ro,n("p",null,[s("Sometimes, due to company policies, some infrastructure requirement or just to further analyze or customize requests, for example, through the usage of tools like "),n("a",ko,[s("fiddler"),a(t)]),s(" and "),n("a",mo,[s("mitmproxy"),a(t)]),s(", you need to specify a proxy server through which HTTP requests are sent to their final destination. This can be easily done with "),vo,s(" method, like in the following example:")]),ho,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[bo]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[go]),_:1})]),_:1}),fo,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[yo]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[wo]),_:1})]),_:1}),jo,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[_o]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[qo]),_:1})]),_:1}),To,n("p",null,[s("Check "),n("a",xo,[s("JdbcJmeterDsl"),a(t)]),s(" for additional details and options and "),n("a",Po,[s("JdbcJmeterDslTest"),a(t)]),s(" for additional examples.")]),So,n("div",Io,[Do,Eo,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[Jo]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[Co]),_:1})]),_:1})]),Ao,n("p",null,[s("Check "),n("a",Mo,[s("DslJsr223Sampler"),a(t)]),s(" for more details and additional options.")]),Oo,n("p",null,[s("Whenever you find some JMeter test element or feature that is not yet supported by the DSL, "),n("strong",null,[s("we strongly encourage you to request it as an issue "),n("a",Lo,[s("here"),a(t)])]),s(" or even contribute it to the DSL (check "),n("a",Ro,[s("Contributing guide"),a(t)]),s(") so the entire community can benefit from it.")]),Go,a(c,null,{default:p(()=>[a(o,{title:"Maven"},{default:p(()=>[No]),_:1}),a(o,{title:"Gradle"},{default:p(()=>[zo]),_:1})]),_:1}),Uo,n("p",null,[s("Check "),n("a",Bo,[s("WrapperJmeterDsl"),a(t)]),s(" for more details and additional wrappers.")]),Ho,n("p",null,[s("This can be helpful to share a Java DSL defined test plan with people not used to the DSL or to use some JMeter feature (or plugin) that is not yet supported by the DSL ("),n("strong",null,[s("but, we strongly encourage you to report it as an issue "),n("a",Wo,[s("here"),a(t)])]),s(" so we can include such support into the DSL for the rest of the community).")]),Fo])}const Ko=l(I,[["render",Vo],["__file","index.html.vue"]]);export{Ko as default}; diff --git a/assets/index.html-a099ade6.js b/assets/index.html-a099ade6.js new file mode 100644 index 00000000..c9ef7b2b --- /dev/null +++ b/assets/index.html-a099ade6.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-8daa1a0e","path":"/","title":"","lang":"en-US","frontmatter":{"home":true,"heroHeight":68,"heroImage":"/logo.svg","actions":[{"text":"User Guide →","link":"/guide/"}],"features":[{"title":"💙 Git, IDE & Programmers Friendly","details":"Simple way of defining performance tests that takes advantage of IDEs autocompletion and inline documentation."},{"title":"💪 JMeter ecosystem & community","details":"Use the most popular performance tool and take advantage of the wide support of protocols and tools."},{"title":"😎 Built-in features & extensibility","details":"Built-in additional features which ease usage (like jmx2dsl and recorder) and CI/CD pipelines integration."}],"footer":"Made by Abstracta with ❤️ | Apache 2.0 Licensed | Powered by Vuepress","footerHtml":true},"headers":[{"level":2,"title":"Example","slug":"example","link":"#example","children":[]},{"level":2,"title":"Hear It From Our Community","slug":"hear-it-from-our-community","link":"#hear-it-from-our-community","children":[]}],"git":{},"filePathRelative":"index.md"}');export{e as data}; diff --git a/assets/index.html-bb230a72.js b/assets/index.html-bb230a72.js new file mode 100644 index 00000000..0de63646 --- /dev/null +++ b/assets/index.html-bb230a72.js @@ -0,0 +1,28 @@ +import{_ as l,r as o,o as r,c as u,a as n,b as t,d as a,w as s,e as d}from"./app-863e42f0.js";const m={},k=d(`

    Example

    Add dependency to your project:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +

    Create performance test:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +            threadGroup(2, 10,
    +                    httpSampler("http://my.service")
    +            )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +
    `,5),h={href:"https://github.com/abstracta/jmeter-java-dsl-sample",target:"_blank",rel:"noopener noreferrer"},v=n("h2",{id:"hear-it-from-our-community",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#hear-it-from-our-community","aria-hidden":"true"},"#"),t(" Hear It From Our Community")],-1),f=n("p",null,"It's a great new functionality. If you don't know about you definitely should check it out.",-1),g=n("p",null,'JMeterDSL is really easy to pick up for a team with some familiar with Java and JMeter. My team was able to learn and start using it within a day! We love it that we can create reusable and extensible "script" components to assemble our performance tests with great ease. The ability to branch and merge (no more XML!) is a breath of fresh air. The ability to run the tests locally or remotely (Azure, in our case) is exactly what we are looking for! Thank you for creating this!',-1),b=n("p",null,"java-jmeter-dsl is a real breath of fresh air for us. This tool gave us a new vision of how tests can be built, how to integrate them into processes and how to automate them. And we can do all this on the basis of the well-known popular and proven Apache JMeter engine. java-jmeter-dsl gives your JMeter-based tests maintainability and reusability at the level of any code solution, which is actually a must for large teams or teams with a large number of tests",-1),w=n("p",null,"I strongly encourage you to check this very promising project and give it a start on GitHub if you like it",-1),y=n("p",null,"As more of a java developer and less of a jmeter engineer, I have always wished this",-1),_=n("p",null,"Demo of the @AbstractaUS Jmeter DSL with @rabelenda is soooooo cooooool",-1),j=n("p",null,"I am an early bird user, it is so elegant, extendable, and flexible even from the first commits, thank you so much, Roger and Team. Java DSL is one love for sure! Inspired me to new ideas, and allowed me to verify the correctness of my theories - Java DSL rock this Jmeter party!!!",-1),I=n("p",null,"La estuve viendo un poco (JMeter DSL), haciendo lo básico! Pero es súper recomendable! Es JMeter pero en tu repo amigo! con Java! 👨🏻‍💻🚀",-1),A=n("p",null,"JMeter DSL brings JMeter to the frontline of technological ingenuity. It solves most of the problems traditionally associated with JMeter such as lack of dependency management, difficulties with source control integration and difficulties to extend.",-1),x={href:"https://pymeter.readthedocs.io/en/latest/",target:"_blank",rel:"noopener noreferrer"},S=n("p",null,"java-jmeter-dsl is a great tool because it can help you reuse your REST-assured tests in order to create some performance test cases. It's very useful because you can generate JTL files to create JMeter reports and even use InfluxDB and Grafana to generate very nice graphics.",-1),J=n("p",null,"It will be the new direction for JMeter for sure",-1),M=n("p",null,"I just started exploring JMeter DSL. It is pretty cool. I will post an article and video soon. #jmeter",-1),T=n("p",null,"Write concise, readable code that's easier to maintain and update using JMeter DSL from Abstracta.",-1),P=n("p",null,"I tried using JMeter-Java-DSL. It's awesome, I can combine Selenium & JMeter library code in single script from comfort of my IDE.",-1),L=n("p",null,"Probé JMeter DSL para Performance Testing y en unas pocas líneas de código puedes ejecutarlo. Realmente muy útil.",-1),D=n("p",null,"JMeter Java DSL is a really useful avatar of the load testing tool. This is specially good for CI/CD projects where developers should be using some form of performance evaluation to validate a good build.",-1),E=n("p",null,"I was looking for simplicity of test plan implementation, reusability of components, assertion on metrics, html report per test, ease of debugging and maintenance, great support and documentation with tons of examples - and I found them here. JMeter DSL covers JMeter's missing functionality and does it great.",-1),C=n("p",null,"I always wished to have easy DSL for jmeter when other tools were supporting such as gatling. I was delighted when jmeter dsl was introduced and tried in my work project. It works so good in simple projects. Establishing CI pipeline is also smooth and easy now in organisation. Thank you for your valuable effort.",-1),B={style:{margin:"10px"}};function G(O,R){const i=o("ExternalLinkIcon"),e=o("testimonial"),p=o("carousel"),c=o("AutoLink");return r(),u("div",null,[k,n("p",null,[t("You can use "),n("a",h,[t("this project"),a(i)]),t(" as a starting point.")]),v,a(p,null,{default:s(()=>[a(e,{item:{source:"https://www.linkedin.com/pulse/oop-automation-testing-jmeter-java-dsl-more-joe-colantonio ",name:" Joe Colantonio ",position:" Founder @ TestGuild"}},{default:s(()=>[f]),_:1},8,["item"]),a(e,{item:{source:"https://github.com/abstracta/jmeter-java-dsl/issues/201#issuecomment-1638715839 ",name:" Mike Liu ",position:" Senior Software Engineering Manager @ MGM Resorts International"}},{default:s(()=>[g]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/in/kirill-yurkov-31b42ba1 ",name:" Kirill Yurkov ",position:" Head of Observability & Reliability @ Самокат"}},{default:s(()=>[b]),_:1},8,["item"]),a(e,{item:{source:"https://octoperf.com/blog/2022/06/13/jmeter-test-as-code ",name:" Gérald Pereira ",position:" CTO @ OctoPerf"}},{default:s(()=>[w]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/posts/krmahadevan_github-abstractajmeter-java-dsl-simple-activity-6848882314567647232-3VG-?utm_source=share&utm_medium=member_desktop ",name:" Krishnan Mahadevan ",position:""}},{default:s(()=>[y]),_:1},8,["item"]),a(e,{item:{source:"https://twitter.com/PerfBytes/status/1540433983237939200 ",name:" PerfBytes ",position:""}},{default:s(()=>[_]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/in/isakovoleksii/ ",name:" Oleksii Isakov ",position:" Software Development Engineer in Test & Performance Analyst"}},{default:s(()=>[j]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/posts/marto-vasconcelo-1910014b_jmeter-scripting-la-pieza-faltante-roger-activity-7031768122055364608-bqGp ",name:" Marto Vasconcelo ",position:" Analista de sistemas de TI @ Cencosud S.A."}},{default:s(()=>[I]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/feed/update/urn:li:ugcPost:6973987279207882752?commentUrn=urn%3Ali%3Acomment%3A%28ugcPost%3A6973987279207882752%2C6973992977908027392%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%286973992977908027392%2Curn%3Ali%3AugcPost%3A6973987279207882752%29 ",name:" Eldad Uzman ",position:" Automation Architect"}},{default:s(()=>[A,n("p",null,[t("That, with addition to the highly engaging attitude of Abstracta, has inspired me into extending the project further by introducing "),n("a",x,[t("PYmeter"),a(i)]),t(", a python DSL package for JMeter based on jmeter-java-dsl.")])]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/in/tamasbojte/ ",name:" Tamás Bőjte ",position:" Senior Automation Engineer"}},{default:s(()=>[S]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/in/uddipan-halder-b5241b84/ ",name:" Uddipan Halder ",position:" Lead Performance Test Engineer"}},{default:s(()=>[J]),_:1},8,["item"]),a(e,{item:{source:"https://twitter.com/QAInsights/status/1520577180744523776 ",name:" NaveenKumar Namachivayam ",position:" Performance Engineer"}},{default:s(()=>[M]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/posts/microsoft-developers_write-concise-readable-code-thats-easier-activity-7079873611678814208-qXl7 ",name:" Microsoft Developer ",position:""}},{default:s(()=>[T]),_:1},8,["item"]),a(e,{item:{source:"https://twitter.com/abhaybharti/status/1552149409802354688 ",name:" Abhay Bharti ",position:" Principal Engineer"}},{default:s(()=>[P]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/posts/pablo-herrera-ec_java-maven-web-activity-6982698450366648320-3rvZ ",name:" Pablo Herrera ",position:" QA Automation Engineer"}},{default:s(()=>[L]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/posts/vishalendupandey_releases-abstractajmeter-java-dsl-activity-7016121555520745472-nv9Y ",name:" Vishalendu Pandey ",position:" Performance Architect"}},{default:s(()=>[D]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/in/rados%C5%82aw-siatka-62a5a66/ ",name:" Radoslaw Siatka ",position:" Test Lead @ GFT Poland"}},{default:s(()=>[E]),_:1},8,["item"]),a(e,{item:{source:"https://www.linkedin.com/in/premraj-murugaraj/ ",name:" Premraj Murugaraj ",position:" Senior Engineer, Testing @ Singtel"}},{default:s(()=>[C]),_:1},8,["item"])]),_:1}),n("div",B,[a(c,{item:{link:"https://forms.gle/h2A7zbHKRiSvCqBd7",text:"Share your testimonial",icon:"fa-solid fa-bullhorn"}},null,8,["item"])])])}const H=l(m,[["render",G],["__file","index.html.vue"]]);export{H as default}; diff --git a/assets/jmdsl-recorder-7b4503f7.gif b/assets/jmdsl-recorder-7b4503f7.gif new file mode 100644 index 00000000..2e64d0d0 Binary files /dev/null and b/assets/jmdsl-recorder-7b4503f7.gif differ diff --git a/assets/jmeter-http-sampler-debugging-4b2e79d2.png b/assets/jmeter-http-sampler-debugging-4b2e79d2.png new file mode 100644 index 00000000..81b503bf Binary files /dev/null and b/assets/jmeter-http-sampler-debugging-4b2e79d2.png differ diff --git a/assets/not-synchronized-samples-483d50c1.png b/assets/not-synchronized-samples-483d50c1.png new file mode 100644 index 00000000..c43f5811 Binary files /dev/null and b/assets/not-synchronized-samples-483d50c1.png differ diff --git a/assets/octoperf-cf8e523e.png b/assets/octoperf-cf8e523e.png new file mode 100644 index 00000000..7e372e58 Binary files /dev/null and b/assets/octoperf-cf8e523e.png differ diff --git a/assets/octoperf-logo-dc518d38.png b/assets/octoperf-logo-dc518d38.png new file mode 100644 index 00000000..d39525e2 Binary files /dev/null and b/assets/octoperf-logo-dc518d38.png differ diff --git a/assets/post-processor-debugging-2735516e.png b/assets/post-processor-debugging-2735516e.png new file mode 100644 index 00000000..26bcd2c6 Binary files /dev/null and b/assets/post-processor-debugging-2735516e.png differ diff --git a/assets/rps-thread-group-timeline-11c83237.png b/assets/rps-thread-group-timeline-11c83237.png new file mode 100644 index 00000000..d43d405e Binary files /dev/null and b/assets/rps-thread-group-timeline-11c83237.png differ diff --git a/assets/search-0782d0d1.svg b/assets/search-0782d0d1.svg new file mode 100644 index 00000000..03d83913 --- /dev/null +++ b/assets/search-0782d0d1.svg @@ -0,0 +1 @@ + diff --git a/assets/style-f6cdd56f.css b/assets/style-f6cdd56f.css new file mode 100644 index 00000000..e5a7dfbe --- /dev/null +++ b/assets/style-f6cdd56f.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-java-dsl/assets/back-to-top-8efcbe56.svg) no-repeat;mask:url(/jmeter-java-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: #5599ff;--c-brand-light: #77adfd;--c-shadow: rgba(0,0,0,.2)}html.dark{--c-brand: #77adfd;--c-brand-light: #5599ff;--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;flex-wrap:wrap;justify-content:center;gap:15px}.grid-logo{display:flex;align-items:center;width:25%;height:70px;padding:15px;border:1px solid var(--c-border);border-radius:5px}.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-java-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)}.carousel{position:relative;box-sizing:border-box;touch-action:pan-y;overscroll-behavior:none;margin:10px 0}.carousel.is-dragging{touch-action:none}.carousel *{box-sizing:border-box}.carousel-viewport{cursor:grab;overflow:auto}.carousel-track{display:flex;position:relative;flex-direction:column;flex-wrap:wrap;align-items:flex-start;gap:20px;height:550px}.carousel-control{--control-size: 20px;box-sizing:content-box;background:transparent;border-radius:0;width:var(--control-size);height:var(--control-size);text-align:center;font-size:var(--control-size);padding:0;color:var(--c-text);display:flex;justify-content:center;align-items:center;position:absolute;border:0;cursor:pointer;margin:0 10px;top:50%;transform:translateY(-50%)}.carousel-control:hover{color:var(--c-text-lighter)}.carousel-control:disabled{cursor:not-allowed;opacity:.5}.carousel-prev{left:0}.carousel-next{right:0}.testimonial{display:flex;flex-direction:column;background-color:var(--c-bg-lighter);border-radius:15px;padding:15px;width:400px;white-space:normal;scroll-snap-stop:auto;flex-shrink:0;justify-content:center;transform:translateZ(0)}.testimonial .position{margin-top:5px;color:var(--c-text-lighter)}.testimonial .comment p{margin:10px 0 0}.testimonial .fa-github{color:var(--c-text)}.testimonial .fa-globe{color:#aaa}.testimonial .fa-linkedin{color:#0a66c2}.testimonial .fa-twitter{color:#1d9bf0} diff --git a/assets/synchronized-samples-3256a15e.png b/assets/synchronized-samples-3256a15e.png new file mode 100644 index 00000000..eec5f6b0 Binary files /dev/null and b/assets/synchronized-samples-3256a15e.png differ diff --git a/assets/test-plan-gui-a4e8b653.png b/assets/test-plan-gui-a4e8b653.png new file mode 100644 index 00000000..5de41983 Binary files /dev/null and b/assets/test-plan-gui-a4e8b653.png differ diff --git a/assets/testimonials.html-0b80892c.js b/assets/testimonials.html-0b80892c.js new file mode 100644 index 00000000..4740cf9c --- /dev/null +++ b/assets/testimonials.html-0b80892c.js @@ -0,0 +1 @@ +import{_ as m,r as i,o as c,c as d,d as t,w as o,a as e,b as n}from"./app-863e42f0.js";const u={},h=e("h2",{id:"hear-it-from-our-community",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#hear-it-from-our-community","aria-hidden":"true"},"#"),n(" Hear It From Our Community")],-1),p=e("p",null,"It's a great new functionality. If you don't know about you definitely should check it out.",-1),f=e("p",null,'JMeterDSL is really easy to pick up for a team with some familiar with Java and JMeter. My team was able to learn and start using it within a day! We love it that we can create reusable and extensible "script" components to assemble our performance tests with great ease. The ability to branch and merge (no more XML!) is a breath of fresh air. The ability to run the tests locally or remotely (Azure, in our case) is exactly what we are looking for! Thank you for creating this!',-1),w=e("p",null,"java-jmeter-dsl is a real breath of fresh air for us. This tool gave us a new vision of how tests can be built, how to integrate them into processes and how to automate them. And we can do all this on the basis of the well-known popular and proven Apache JMeter engine. java-jmeter-dsl gives your JMeter-based tests maintainability and reusability at the level of any code solution, which is actually a must for large teams or teams with a large number of tests",-1),_=e("p",null,"I strongly encourage you to check this very promising project and give it a start on GitHub if you like it",-1),g=e("p",null,"As more of a java developer and less of a jmeter engineer, I have always wished this",-1),y=e("p",null,"Demo of the @AbstractaUS Jmeter DSL with @rabelenda is soooooo cooooool",-1),b=e("p",null,"I am an early bird user, it is so elegant, extendable, and flexible even from the first commits, thank you so much, Roger and Team. Java DSL is one love for sure! Inspired me to new ideas, and allowed me to verify the correctness of my theories - Java DSL rock this Jmeter party!!!",-1),v=e("p",null,"La estuve viendo un poco (JMeter DSL), haciendo lo básico! Pero es súper recomendable! Es JMeter pero en tu repo amigo! con Java! 👨🏻‍💻🚀",-1),k=e("p",null,"JMeter DSL brings JMeter to the frontline of technological ingenuity. It solves most of the problems traditionally associated with JMeter such as lack of dependency management, difficulties with source control integration and difficulties to extend.",-1),j={href:"https://pymeter.readthedocs.io/en/latest/",target:"_blank",rel:"noopener noreferrer"},A=e("p",null,"java-jmeter-dsl is a great tool because it can help you reuse your REST-assured tests in order to create some performance test cases. It's very useful because you can generate JTL files to create JMeter reports and even use InfluxDB and Grafana to generate very nice graphics.",-1),I=e("p",null,"It will be the new direction for JMeter for sure",-1),J=e("p",null,"I just started exploring JMeter DSL. It is pretty cool. I will post an article and video soon. #jmeter",-1),M=e("p",null,"Write concise, readable code that's easier to maintain and update using JMeter DSL from Abstracta.",-1),S=e("p",null,"I tried using JMeter-Java-DSL. It's awesome, I can combine Selenium & JMeter library code in single script from comfort of my IDE.",-1),L=e("p",null,"Probé JMeter DSL para Performance Testing y en unas pocas líneas de código puedes ejecutarlo. Realmente muy útil.",-1),T=e("p",null,"JMeter Java DSL is a really useful avatar of the load testing tool. This is specially good for CI/CD projects where developers should be using some form of performance evaluation to validate a good build.",-1),D=e("p",null,"I was looking for simplicity of test plan implementation, reusability of components, assertion on metrics, html report per test, ease of debugging and maintenance, great support and documentation with tons of examples - and I found them here. JMeter DSL covers JMeter's missing functionality and does it great.",-1),P=e("p",null,"I always wished to have easy DSL for jmeter when other tools were supporting such as gatling. I was delighted when jmeter dsl was introduced and tried in my work project. It works so good in simple projects. Establishing CI pipeline is also smooth and easy now in organisation. Thank you for your valuable effort.",-1),x={style:{margin:"10px"}};function E(C,B){const a=i("testimonial"),s=i("ExternalLinkIcon"),r=i("carousel"),l=i("AutoLink");return c(),d("div",null,[h,t(r,null,{default:o(()=>[t(a,{item:{source:"https://www.linkedin.com/pulse/oop-automation-testing-jmeter-java-dsl-more-joe-colantonio ",name:" Joe Colantonio ",position:" Founder @ TestGuild"}},{default:o(()=>[p]),_:1},8,["item"]),t(a,{item:{source:"https://github.com/abstracta/jmeter-java-dsl/issues/201#issuecomment-1638715839 ",name:" Mike Liu ",position:" Senior Software Engineering Manager @ MGM Resorts International"}},{default:o(()=>[f]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/in/kirill-yurkov-31b42ba1 ",name:" Kirill Yurkov ",position:" Head of Observability & Reliability @ Самокат"}},{default:o(()=>[w]),_:1},8,["item"]),t(a,{item:{source:"https://octoperf.com/blog/2022/06/13/jmeter-test-as-code ",name:" Gérald Pereira ",position:" CTO @ OctoPerf"}},{default:o(()=>[_]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/posts/krmahadevan_github-abstractajmeter-java-dsl-simple-activity-6848882314567647232-3VG-?utm_source=share&utm_medium=member_desktop ",name:" Krishnan Mahadevan ",position:""}},{default:o(()=>[g]),_:1},8,["item"]),t(a,{item:{source:"https://twitter.com/PerfBytes/status/1540433983237939200 ",name:" PerfBytes ",position:""}},{default:o(()=>[y]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/in/isakovoleksii/ ",name:" Oleksii Isakov ",position:" Software Development Engineer in Test & Performance Analyst"}},{default:o(()=>[b]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/posts/marto-vasconcelo-1910014b_jmeter-scripting-la-pieza-faltante-roger-activity-7031768122055364608-bqGp ",name:" Marto Vasconcelo ",position:" Analista de sistemas de TI @ Cencosud S.A."}},{default:o(()=>[v]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/feed/update/urn:li:ugcPost:6973987279207882752?commentUrn=urn%3Ali%3Acomment%3A%28ugcPost%3A6973987279207882752%2C6973992977908027392%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%286973992977908027392%2Curn%3Ali%3AugcPost%3A6973987279207882752%29 ",name:" Eldad Uzman ",position:" Automation Architect"}},{default:o(()=>[k,e("p",null,[n("That, with addition to the highly engaging attitude of Abstracta, has inspired me into extending the project further by introducing "),e("a",j,[n("PYmeter"),t(s)]),n(", a python DSL package for JMeter based on jmeter-java-dsl.")])]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/in/tamasbojte/ ",name:" Tamás Bőjte ",position:" Senior Automation Engineer"}},{default:o(()=>[A]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/in/uddipan-halder-b5241b84/ ",name:" Uddipan Halder ",position:" Lead Performance Test Engineer"}},{default:o(()=>[I]),_:1},8,["item"]),t(a,{item:{source:"https://twitter.com/QAInsights/status/1520577180744523776 ",name:" NaveenKumar Namachivayam ",position:" Performance Engineer"}},{default:o(()=>[J]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/posts/microsoft-developers_write-concise-readable-code-thats-easier-activity-7079873611678814208-qXl7 ",name:" Microsoft Developer ",position:""}},{default:o(()=>[M]),_:1},8,["item"]),t(a,{item:{source:"https://twitter.com/abhaybharti/status/1552149409802354688 ",name:" Abhay Bharti ",position:" Principal Engineer"}},{default:o(()=>[S]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/posts/pablo-herrera-ec_java-maven-web-activity-6982698450366648320-3rvZ ",name:" Pablo Herrera ",position:" QA Automation Engineer"}},{default:o(()=>[L]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/posts/vishalendupandey_releases-abstractajmeter-java-dsl-activity-7016121555520745472-nv9Y ",name:" Vishalendu Pandey ",position:" Performance Architect"}},{default:o(()=>[T]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/in/rados%C5%82aw-siatka-62a5a66/ ",name:" Radoslaw Siatka ",position:" Test Lead @ GFT Poland"}},{default:o(()=>[D]),_:1},8,["item"]),t(a,{item:{source:"https://www.linkedin.com/in/premraj-murugaraj/ ",name:" Premraj Murugaraj ",position:" Senior Engineer, Testing @ Singtel"}},{default:o(()=>[P]),_:1},8,["item"])]),_:1}),e("div",x,[t(l,{item:{link:"https://forms.gle/h2A7zbHKRiSvCqBd7",text:"Share your testimonial",icon:"fa-solid fa-bullhorn"}},null,8,["item"])])])}const R=m(u,[["render",E],["__file","testimonials.html.vue"]]);export{R as default}; diff --git a/assets/testimonials.html-96ecbae2.js b/assets/testimonials.html-96ecbae2.js new file mode 100644 index 00000000..cc83d711 --- /dev/null +++ b/assets/testimonials.html-96ecbae2.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-7e348068","path":"/testimonials.html","title":"","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"Hear It From Our Community","slug":"hear-it-from-our-community","link":"#hear-it-from-our-community","children":[]}],"git":{},"filePathRelative":"testimonials.md"}');export{t as data}; diff --git a/assets/ultimate-thread-group-gui-400bcb90.png b/assets/ultimate-thread-group-gui-400bcb90.png new file mode 100644 index 00000000..8703fb17 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 00000000..71073cc0 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 00000000..77bcf22c 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 00000000..66de417c Binary files /dev/null and b/favicon.ico differ diff --git a/guide/index.html b/guide/index.html new file mode 100644 index 00000000..c43a3241 --- /dev/null +++ b/guide/index.html @@ -0,0 +1,2354 @@ + + + + + + + + + User guide | jmeter-java-dsl + + + + +

    User guide

    Here we share some tips and examples on how to use the DSL to tackle common use cases.

    Provided examples use JUnit 5open in new window and AssertJopen in new window, but you can use other test & assertion libraries.

    Explore the DSL in your preferred IDE to discover all available features, and consider reviewing existing testsopen in new window for additional examples.

    The DSL currently supports most common used cases, keeping it simple and avoiding investing development effort in features that might not be needed. 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 issueopen in new window and we will try to implement it as soon as possible. Usually porting JMeter features is quite fast.

    TIP

    If you like this project, please give it a star ⭐ in GitHubopen in new window! 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 documentationopen in new window.

    Setup

    To use the DSL just include it in your project:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation("us.abstracta.jmeter:jmeter-java-dsl:1.29") {
    +    exclude("org.apache.jmeter", "bom")
    +}
    +

    TIP

    Hereopen in new window is a sample project in case you want to start one from scratch.

    Simple HTTP test plan

    To generate HTTP requests just use provided httpSampler.

    The following example uses 2 threads (concurrent users) that send 10 HTTP GET requests each to http://my.service.

    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.

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import java.time.Instant;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        //this is just to log details of each request stats
    +        jtlWriter("target/jtls")
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    When working with multiple samplers in a test plan, specify their names (eg: httpSampler("home", "http://my.service")) to easily check their respective statistics.

    TIP

    Set connection and response timeouts to avoid potential execution differences when running test plan in different machines. Here are more details.

    TIP

    Since JMeter uses log4j2open in new window, if you want to control the logging level or output, you can use something similar to this log4j2.xmlopen in new window.

    TIP

    Keep in mind that you can use Java 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 exampleopen in new window of some complex abstraction built using Java features and the DSL.

    Check HTTP performance testing for additional details while testing HTTP services.

    DSL recorder

    When creating test plans you can rely just on the IDE or you can use provided recorder.

    Here is a small demo using it:

    jmdsl recorder demo

    TIP

    You can use jbangopen in new window to easily execute the recorder with the latest version available. E.g.:

    jbang us.abstracta.jmeter:jmeter-java-dsl-cli:1.29 recorder http://retailstore.test
    +

    TIP

    Use java -jar jmdsl.jar help recorder to see the list of options to customize your recording.

    TIP

    In general use ---url-includes to ignore URLs that are not relevant to the performance test.

    WARNING

    Unlike the rest of JMeter DSL, which is compiled with Java 8, jmdsl.jar and us.abstracta.jmeter:jmeter-java-dsl-cli are compiled with Java 11 due to some dependencies requirement (latest Selenium drivers mainly).

    So, to run above commands, you will need Java 11 or newer.

    Correlations

    To avoid fragile test plans with fixed values in request parameters, the DSL recorder, through the usage of the JMeter Correlation Recorder Pluginopen in new window, allows you to define correlation rules.

    Correlation rules define regular expressions, which allow the recorder to automatically add regexExtractor and replace occurrences of extracted values in following requests with proper variable references.

    For example, for the same scenario previously shown, and using --config option (which makes correlation rules easier to maintain) with following file:

    recorder:
    +  url: http://retailstore.test
    +  urlIncludes:
    +    - retailstore.test.*
    +  correlations:
    +    - variable: productId
    +      extractor: name="productId" value="([^"]+)"
    +      replacement: productId=(.*)
    +

    We get this test plan:

    ///usr/bin/env jbang "$0" "$@" ; exit $?
    +/*
    +These commented lines make the class executable if you have jbang installed by making the file
    +executable (eg: chmod +x ./PerformanceTest.java) and just executing it with ./PerformanceTest.java
    +*/
    +//DEPS org.assertj:assertj-core:3.23.1
    +//DEPS org.junit.jupiter:junit-jupiter-engine:5.9.1
    +//DEPS org.junit.platform:junit-platform-launcher:1.9.1
    +//DEPS us.abstracta.jmeter:jmeter-java-dsl:1.29
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.io.PrintWriter;
    +import java.nio.charset.StandardCharsets;
    +import org.apache.http.entity.ContentType;
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.junit.jupiter.api.Test;
    +import org.junit.platform.engine.discovery.DiscoverySelectors;
    +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
    +import org.junit.platform.launcher.core.LauncherFactory;
    +import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
    +import org.junit.platform.launcher.listeners.TestExecutionSummary;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(1, 1,
    +          httpDefaults()
    +            .encoding(StandardCharsets.UTF_8),
    +          httpSampler("/-1", "http://retailstore.test"),
    +          httpSampler("/home-3", "http://retailstore.test/home")
    +            .children(
    +              regexExtractor("productId#2", "name=\"productId\" value=\"([^\"]+)\"")
    +                .defaultValue("productId#2_NOT_FOUND")
    +            ),
    +          httpSampler("/cart-16", "http://retailstore.test/cart")
    +            .method(HTTPConstants.POST)
    +            .contentType(ContentType.APPLICATION_FORM_URLENCODED)
    +            .rawParam("productId", "${productId#2}"),
    +          httpSampler("/cart-17", "http://retailstore.test/cart")
    +        )
    +    ).run();
    +    assertThat(stats.overall().errorsCount()).isEqualTo(0);
    +  }
    +
    +  /*
    +   This method is only included to make the test class self-executable. You can remove it when
    +   executing tests with maven, gradle, or some other tool.
    +   */
    +  public static void main(String[] args) {
    +    SummaryGeneratingListener summaryListener = new SummaryGeneratingListener();
    +    LauncherFactory.create()
    +        .execute(LauncherDiscoveryRequestBuilder.request()
    +                .selectors(DiscoverySelectors.selectClass(PerformanceTest.class))
    +                .build(),
    +            summaryListener);
    +    TestExecutionSummary summary = summaryListener.getSummary();
    +    summary.printFailuresTo(new PrintWriter(System.err));
    +    System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0);
    +  }
    +
    +}
    +

    In this test plan you can see an already added an extractor and the usage of extracted value in a subsequent request (as a variable reference).

    TIP

    To identify potential correlations, you can check in request parameters or URLs with fixed values and then, check the automatically created recording .jtl file (by default in target/recording folder) to identify proper regular expression for extraction.

    We have ideas to ease this for the future, but, if you have ideas, or just want to give more priority to improving this, please create an issue in the repositoryopen in new window to let us know.

    TIP

    When using --config, take advantage of your IDEs auto-completion and inline documentation capabilities by using .jmdsl.yml suffix in config file names.

    Here is a screenshot of autocompletion in action:

    Config file IDE autocomplete

    DSL code generation from JMX file

    To ease migrating existing JMeter test plans and ease learning about DSL features, the DSL provides jmx2dsl cli command (download the latest cli version from releases pageopen in new window or use jbangopen in new window) command line tool which you can use to generate DSL code from existing JMX files.

    As an example:

    java -jar jmdsl.jar jmx2dsl test-plan.jmx
    +
    jbang us.abstracta.jmeter:jmeter-java-dsl-cli:1.29 jmx2dsl test-plan.jmx
    +

    Could generate something like the following output:

    ///usr/bin/env jbang "$0" "$@" ; exit $?
    +/*
    +These commented lines make the class executable if you have jbang installed by making the file
    +executable (eg: chmod +x ./PerformanceTest.java) and just executing it with ./PerformanceTest.java
    +*/
    +//DEPS org.assertj:assertj-core:3.23.1
    +//DEPS org.junit.jupiter:junit-jupiter-engine:5.9.1
    +//DEPS org.junit.platform:junit-platform-launcher:1.9.1
    +//DEPS us.abstracta.jmeter:jmeter-java-dsl:1.29
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.io.PrintWriter;
    +import org.junit.jupiter.api.Test;
    +import org.junit.platform.engine.discovery.DiscoverySelectors;
    +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
    +import org.junit.platform.launcher.core.LauncherFactory;
    +import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
    +import org.junit.platform.launcher.listeners.TestExecutionSummary;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        jtlWriter("target/jtls")
    +    ).run();
    +    assertThat(stats.overall().errorsCount()).isEqualTo(0);
    +  }
    +
    +  /*
    +   This method is only included to make the test class self-executable. You can remove it when
    +   executing tests with maven, gradle, or some other tool.
    +   */
    +  public static void main(String[] args) {
    +    SummaryGeneratingListener summaryListener = new SummaryGeneratingListener();
    +    LauncherFactory.create()
    +        .execute(LauncherDiscoveryRequestBuilder.request()
    +                .selectors(DiscoverySelectors.selectClass(PerformanceTest.class))
    +                .build(),
    +            summaryListener);
    +    TestExecutionSummary summary = summaryListener.getSummary();
    +    summary.printFailuresTo(new PrintWriter(System.err));
    +    System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0);
    +  }
    +
    +}
    +

    WARNING

    Unlike the rest of JMeter DSL which is compiled with Java 8, jmdsl.jar and us.abstracta.jmeter:jmeter-java-dsl-cli are compiled with Java 11 due to some dependencies requirement (latest Selenium drivers mainly).

    So, to run above commands, you will need Java 11 or newer.

    TIP

    Review and try generated code before executing it as is. I.e: tune thread groups and iterations to 1 to give it a try.

    TIP

    Always review generated DSL code. You should add proper assertions to it, might want to clean it up, add to your maven or gradle project dependencies listed on initial comments of generated code, modularize it better, check that conversion is accurate according to DSL, or even propose improvements for it in the GitHub repository.

    TIP

    Conversions can always be improved, and since there are many combinations and particular use cases, different semantics, etc, getting a perfect conversion for every scenario can get tricky.

    If you find any potential improvement to code generation, please help us by creating an issueopen in new window or discussionopen in new window in GitHub repository.

    Run test at scale

    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.

    BlazeMeter

    By including the following module as a dependency:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-blazemeter</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-blazemeter:1.29'
    +

    You can easily run a JMeter test plan at scale in BlazeMeteropen in new window like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.blazemeter.BlazeMeterEngine;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    TestPlanStats 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(System.getenv("BZ_TOKEN"))
    +        .testName("DSL test")
    +        .totalUsers(500)
    +        .holdFor(Duration.ofMinutes(10))
    +        .threadsPerEngine(100)
    +        .testTimeout(Duration.ofMinutes(20)));
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    This test is using BZ_TOKEN, a custom environment variable with <KEY_ID>:<KEY_SECRET> format, to get the BlazeMeter API authentication credentials.

    Note that is as simple as generating a BlazeMeter authentication tokenopen in new window and adding .runIn(new BlazeMeterEngine(...)) to any existing jmeter-java-dsl test to get it running at scale in BlazeMeter.

    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:

    BlazeMeter Example Execution Dashboard

    Check BlazeMeterEngineopen in new window 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

    BlazeMeterEngine always returns 0 as sentBytes statistics since there is no efficient way to get it from BlazMeter.

    TIP

    BlazeMeterEngine will automatically upload to BlazeMeter files used in csvDataSet and httpSampler with bodyFile or bodyFilePart methods.

    For example this test plan works out of the box (no need for uploading referenced files or adapt test plan):

    testPlan(
    +    threadGroup(100, Duration.ofMinutes(5),
    +      csvDataSet(new TestResource("users.csv")),
    +      httpSampler(SAMPLE_LABEL, "https://myservice/users/${USER}")
    +    )
    +).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN"))
    +    .testTimeout(Duration.ofMinutes(10)));
    +

    If you need additional files to be uploaded to BlazeMeter, you can easily specify them with the BlazemeterEngine.assets() method.

    TIP

    By default BlazeMeterEngine will run tests from default location (most of the times us-east4-a). But in some scenarios you might want to change the location, or even run the test from multiple locations.

    Here is an example how you can easily set this up:

    testPlan(
    +    threadGroup(300, Duration.ofMinutes(5), // 300 total users for 5 minutes
    +      httpSampler(SAMPLE_LABEL, "https://myservice")
    +    )
    +).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN"))
    +    .location(BlazeMeterLocation.GCP_SAO_PAULO, 30) // 30% = 90 users will run in Google Cloud Platform at Sao Paulo
    +    .location("MyPrivateLocation", 70) // 70% = 210 users will run in MyPrivateLocation named private location
    +    .testTimeout(Duration.ofMinutes(10)));
    +

    TIP

    In case you want to get debug logs for HTTP calls to BlazeMeter API, you can include the following setting to an existing log4j2.xml configuration file:

    <Logger name="us.abstracta.jmeter.javadsl.blazemeter.BlazeMeterClient" level="DEBUG"/>
    +<Logger name="okhttp3" level="DEBUG"/>
    +

    WARNING

    If you use test elements (JSR223 elements, httpSamplers, ifController or whileController) with Java lambdas instead of strings, check this section of the user guide to use them while running test plan in BlazeMeter.

    OctoPerf

    In the same fashion as with BlazeMeter, just by including the following module as a dependency:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-octoperf</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-octoperf:1.29'
    +

    You can easily run a JMeter test plan at scale in OctoPerfopen in new window like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.octoperf.OctoPerfEngine;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    TestPlanStats stats = testPlan(
    +        // number of threads and iterations are in the end overwritten by OctoPerf engine settings 
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        )
    +    ).runIn(new OctoPerfEngine(System.getenv("OCTOPERF_API_KEY"))
    +        .projectName("DSL test")
    +        .totalUsers(500)
    +        .rampUpFor(Duration.ofMinutes(1))
    +        .holdFor(Duration.ofMinutes(10))
    +        .testTimeout(Duration.ofMinutes(20)));
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    This test is using OCTOPERF_API_KEY, a custom environment variable containing an OctoPerf API key.

    Note that, as with the BlazeMeter case, it is as simple as getting the OctoPerf API keyopen in new window and adding .runIn(new OctoPerfEngine(...)) to any existing jmeter-java-dsl test to get it running at scale in OctoPerf.

    As with the BlazeMeter case, with OctoPerf 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 OctoPerf:

    OctoPerf Example Execution Dashboard

    Check OctoPerfEngineopen in new window for details on usage and available settings when running tests in OctoPerf.

    WARNING

    To avoid piling up virtual users and scenarios in OctoPerf project, OctoPerfEngine deletes any OctoPerfEngine previously created entities (virtual users and scenarios with jmeter-java-dsl tag) in the project.

    It is very important that you use different project names for different projects to avoid interference (parallel execution of two jmeter-java-dsl projects).

    If you want to disable this automatic cleanup, you can use the existing OctoPerfEngine method .projectCleanUp(false).

    TIP

    In case you want to get debug logs for HTTP calls to OctoPerf API, you can include the following setting to an existing log4j2.xml configuration file:

    <Logger name="us.abstracta.jmeter.javadsl.octoperf.OctoPerfClient" level="DEBUG"/>
    +<Logger name="okhttp3" level="DEBUG"/>
    +

    WARNING

    There is currently no built-in support for test elements with Java lambdas in OctoPerfEngine (as there is for BlazeMeterEngine). If you need it, please request it by creating a GitHub issueopen in new window.

    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 OctoPerf execution not detected by the client, and avoid keeping the test indefinitely running until is interrupted by a user, which 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 OctoPerf test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or unnecessary waits when there is some unexpected issue with OctoPerf execution.

    Azure Load Testing

    To use Azure Load Testingopen in new window to execute your test plans at scale, is as easy as including the following module as a dependency:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-azure</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-azure:1.29'
    +

    And using the provided engine like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.azure.AzureEngine;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        )
    +    ).runIn(new AzureEngine(System.getenv("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(Duration.ofMinutes(20)));
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    This test is using AZURE_CREDS, a custom environment variable containing tenantId:clientId:clientSecret with proper values for each. Check in Azure Portal tenant propertiesopen in new window the proper tenant ID for your subscription, and follow this guideopen in new window to register an application with proper permissions and secrets generation for tests execution.

    As with the BlazeMeter and OctoPerf, 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:

    Azure Load Testing Example Execution Dashboard

    Check AzureEngineopen in new window for details on usage and available settings when running tests in Azure Load Testing.

    TIP

    AzureEngine will automatically upload to Azure Load Testing files used in csvDataSet and httpSampler with bodyFile or bodyFilePart methods.

    For example this test plan works out of the box (no need for uploading referenced files or adapt test plan):

    testPlan(
    +    threadGroup(100, Duration.ofMinutes(5),
    +      csvDataSet(new TestResource("users.csv")),
    +      httpSampler(SAMPLE_LABEL, "https://myservice/users/${USER}")
    +    )
    +).runIn(new AzureEngine(System.getenv("BZ_TOKEN"))
    +    .testTimeout(Duration.ofMinutes(10)));
    +

    If you need additional files to be uploaded to Azure Load Testing, you can easily specify them with the AzureEngine.assets() method.

    TIP

    If you use a csvDataSet and multiple Azure engines (through the engines() method) and want to split provided CSVs between the Azure engines, as to not generate same requests from each engine, then you can use splitCsvsBetweenEngines.

    TIP

    If you want to correlate test runs to other entities (like a CI/CD job id, product version release, git commit, etc) you can add such information in the test run name by using the testRunName() method.

    TIP

    To get a full view in Azure Load Testing test run execution report not only of the performance test collected metrics, but also metrics from the application components under test, you can register all the application components using the monitoredResources() method.

    monitoredResources() requires a list of resources ids, which you can get by navigating in Azure portal to the correct resource, and then copy part of the url from the browser. For example, a resource id for a container app looks like /subscriptions/my-subscription-id/resourceGroups/my-resource-group/providers/Microsoft.App/containerapps/my-papp.

    TIP

    As with BlazeMeter and OctoPerf cases, if you want to get debug logs for HTTP calls to Azure API, you can include the following setting to an existing log4j2.xml configuration file:

    <Logger name="us.abstracta.jmeter.javadsl.azure.AzureClient" level="DEBUG"/>
    +<Logger name="okhttp3" level="DEBUG"/>
    +

    WARNING

    There is currently no built-in support for test elements with Java lambdas in AzureEngine (as there is for BlazeMeterEngine). If you need it, please request it by creating a GitHub issueopen in new window.

    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 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 in unnecessary expenses in Azure 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 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.

    JMeter remote testing

    JMeter already provides means to run a test on several machines controlled by one master/client machine. This is referred as Remote Testingopen in new window.

    JMeter remote testing requires setting up nodes in server/slave mode (using bin/jmeter-server JMeter script) with a configured keystore (usually rmi_keystore.jks, generated with bin/ JMeter script) which will execute a test plan triggered in a client/master node.

    You can trigger such tests with the DSL using DistributedJmeterEngine as in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.engines.DistributedJmeterEngine;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(200, Duration.ofMinutes(10),
    +            httpSampler("http://my.service")
    +        )
    +    ).runIn(new DistributedJmeterEngine("host1", "host2"));
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    This will run 200 users for 10 minutes on each server/slave (host1 and host2) and aggregate all the results in returned stats.

    WARNING

    Use same version used by JMeter DSL when setting up the cluster to avoid any potential issues.

    For instance, JMeter 5.6 has introduced some changes that currently break some plugins using by JMeter DSL, or change default behavior for test plans.

    To find out the current version of JMeter DSL you can check JMeter jars version in your project dependency tree. E.g.:

    mvn dependency:tree -Dincludes=org.apache.jmeter:ApacheJMeter_core
    +

    Or check JMeter DSL pom.xml property jmeter.versionopen in new window.

    WARNING

    To be able to run the test you require the rmi_keystore.jks file in the working directory of the test. For the time being, we couldn't find a way to allow setting any arbitrary path for the file.

    WARNING

    In general, prefer using BlazeMeter, OctoPerf or Azure options which avoid all the setup and maintenance costs of the infrastructure required by JMeter remote testing, also benefiting from other additional useful features they provide (like reporting capabilities).

    TIP

    Hereopen in new window is an example project using docker-compose that starts a JMeter server/slave and executes a test with it. If you want to do a similar setup, generate your own keystore and properly tune RMI remote server in server/slave.

    Check DistributedJmeterEngineopen in new window and JMeter documentationopen in new window for proper setup and additional options.

    Auto Stop

    As previously shown, it is quite easy to check after test plan execution if the collected metrics are the expected ones and fail/pass the test accordingly.

    But, what if you want to stop your test plan as soon as the metrics deviate from expected ones? This could help avoiding unnecessary resource usage, especially when conducting tests at scale to avoid incurring additional costs.

    With JMeter DSL you can easily define auto-stop conditions over collected metrics, that when met will stop the test plan and throw an exception that will make your test fail.

    Here is an example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.core.listeners.AutoStopListener.AutoStopCondition.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, Duration.ofMinutes(1),
    +          httpSampler("http://my.service")
    +        ),
    +        autoStop()
    +          .when(errors().total().greaterThan(0)) // when any sample fails, then test plan will stop and an exception will be thrown pointing to this condition.
    +    ).run();
    +  }
    +
    +}
    +
    +

    Check AutoStopListeneropen in new window for details on available options for auto-stop conditions.

    autoStop is inspired in JMeter AutoStop Pluginopen in new window, but provides a lot more flexibility.

    TIP

    autoStop will only consider samples within its scope.

    If you place it as a test plan child, then it will evaluate metrics for all samples. If you place it as a thread group child, then it will evaluate metrics for samples of such thread group. If you place it as a controller child, then only samples within such controller. And, if you place it as a sampler child, it will only evaluate samples for that particular sampler.

    Additionally, you can use the samplesMatching(regex) method to only evaluate metrics for a subset of samples within a given scope (eg: all samples with a label starting with users).

    TIP

    You can add multiple autoStop elements within a test plan. The first one containing a condition that is met will trigger the auto-stop.

    To identify which autoStop element triggered, you can specify a name, like autoStop("login"), and the associated name will be included in the exception thrown by autoStop when the test plan is stopped.

    Additionally, you can specify several conditions on an autoStop element. When any of such conditions are met, then the test plan is stopped.

    TIP

    By default, autoStop will evaluate each condition for each sample and stop the test plan as soon as a condition is met.

    This behavior is different from JMeter AutoStop Pluginopen in new window, which evaluates and resets aggregations (it only provides average aggregation) for every second.

    To change this behavior you can use the every(Duration) method (after specifying the aggregation method, eg errors().perSecond().every(Duration.ofSeconds(5)))) to specify that the condition should only be evaluated, and the aggregation reset, for every given period.

    This is particularly helpful for some aggregations (like mean, perSecond, and percent) which may get "stuck" due to historical values collected for the metric.

    As an example to illustrate this issue, consider the scenario where after 10 minutes you get 10k requests with an average sample time of 1 second, but in the last 10 seconds you get 10 requests with an average of 10 seconds. In this scenario, the general average will not be much affected by the last seconds, but you would in any case want to stop the test plan since last seconds average has been way up the expected value. This is a clear scenario where you would like to use the every() method.

    TIP

    By default, autoStop will stop the test plan as soon as the condition is met, but in many cases it is better to wait for the condition to be met for some period of time, to avoid some intermittent or short-lived condition. To not stop the test plan until the condition holds for a given period of time, you can use holdsFor(Duration) at the end of your condition.

    WARNING

    autoStop will automatically work with AzureEngine. But no support has been implemented yet for BlazeMeterEngine or OctoPerfEngine. If you need such support, please create an issue in the GitHub repositoryopen in new window.

    Advanced threads configuration

    jmeter-java-dsl provides two simple ways of creating thread groups which are used in most scenarios:

    • specifying threads and the number of iterations each thread should execute before ending the test plan
    • specifying threads and duration for which each thread should execute before the test plan ends

    This is how they look in code:

    threadGroup(10, 20, ...) // 10 threads for 20 iterations each
    +threadGroup(10, Duration.ofSeconds(20), ...) // 10 threads for 20 seconds each
    +

    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).

    Thread ramps and holds

    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:

    threadGroup().rampTo(10, Duration.ofSeconds(5)).holdIterating(20) // ramp to 10 threads for 5 seconds (1 thread every half second) and iterating each thread 20 times
    +threadGroup().rampToAndHold(10, Duration.ofSeconds(5), Duration.ofSeconds(20)) //similar as above but after ramping up holding execution for 20 seconds
    +

    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:

    threadGroup()
    +    .rampToAndHold(10, Duration.ofSeconds(5), Duration.ofSeconds(20))
    +    .rampToAndHold(100, Duration.ofSeconds(10), Duration.ofSeconds(30))
    +    .rampTo(200, Duration.ofSeconds(10))
    +    .rampToAndHold(100, Duration.ofSeconds(10), Duration.ofSeconds(30))
    +    .rampTo(0, Duration.ofSeconds(5))
    +    .children(
    +      httpSampler("http://my.service")
    +    )
    +

    Which would translate into the following threads' timeline:

    Thread Group Timeline

    Check DslDefaultThreadGroupopen in new window for more details.

    TIP

    To visualize the threads timeline, for complex thread group configurations like the previous one, you can get a chart like the previous one by using provided DslThreadGroup.showTimeline() method.

    TIP

    If you are a JMeter GUI user, you may even be interested in using provided 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).

    For example, for the above test plan you would get a window like the following one:

    UltimateThreadGroup GUI

    TIP

    When using multiple thread groups in a test plan, consider setting a name (eg: threadGroup("main", 1, 1, ...)) on them to properly identify associated requests in statistics & jtl results.

    Throughput based thread group

    Sometimes you want to focus just on the number of requests per second to generate and don't want to be concerned about how many concurrent threads/users, and pauses between requests, are needed. For these scenarios you can use rpsThreadGroup like in the following example:

    rpsThreadGroup()
    +    .maxThreads(500)
    +    .rampTo(20, Duration.ofSeconds(10))
    +    .rampTo(10, Duration.ofSeconds(10))
    +    .rampToAndHold(1000, Duration.ofSeconds(5), Duration.ofSeconds(10))
    +    .children(
    +      httpSampler("http://my.service")
    +    )
    +

    This will internally use JMeter Concurrency Thread Groupopen in new window element in combination with Throughput Shaping Timeopen in new window.

    TIP

    rpsThreadGroup will dynamically create and remove threads and add delays between requests to match the traffic to the expected RPS. You can also specify to control iterations per second (the number of times the flow in the thread group runs per second) instead of threads by using .counting(RpsThreadGroup.EventType.ITERATIONS).

    WARNING

    RPS values control how often to adjust threads and waits. Avoid too low (eg: under 1) values which can cause big waits and don't match the expected RPS.

    JMeter Throughput Shaping Timer calculates each time the delay to be used not taking into consideration future expected RPS. For instance, if you configure 1 thread with a ramp from 0.01 to 10 RPS with 10 seconds duration, when 1 request is sent it will calculate that to match 0.01 RPS has to wait requestsCount/expectedRPS = 1/0.01 = 100 seconds, which would keep the thread stuck for 100 seconds when in fact should have done two additional requests after waiting 1 second (to match the ramp). Setting this value greater or equal to 1 will assure at least 1 evaluation every second.

    WARNING

    When no maxThreads are specified, rpsThreadGroup will use as many threads as needed. In such scenarios, you might face an unexpected number of threads with associated CPU and Memory requirements, which may affect the performance test metrics. You should always set maximum threads to use to avoid such scenarios.

    You can use the following formula to calculate a value for maxThreads: T*R, being T the maximum RPS that you want to achieve and R the maximum expected response time (or iteration time if you use .counting(RpsThreadGroup.EventType.ITERATIONS)) in seconds.

    TIP

    As with the default thread group, with rpsThreadGroup you can use showTimeline to get a chart of configured RPS profile for easy visualization. An example chart:

    RPS Thread Group Timeline

    Check RpsThreadGroupopen in new window for more details.

    Set up & tear down

    When you need to run some custom logic before or after a test plan, the simplest approach is just adding plain java code to it, or using your test framework (eg: JUnit) provided features for this purpose. Eg:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.api.AfterEach;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @BeforeEach
    +  public void setup() {
    +    // my custom setup logic
    +  }
    +
    +  @AfterEach
    +  public void setup() {
    +    // my custom setup logic
    +  }
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    But, in some cases you may need the logic to run inside the JMeter execution context (eg: set some JMeter properties), or, when the test plan runs at scale, to run in the same host where the test plan runs (for example to use some common file).

    In such scenarios you can use provided setupThreadGroup & teardownThreadGroup like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        setupThreadGroup(
    +            httpSampler("http://my.service/tokens")
    +                .method(HTTPConstants.POST)
    +                .children(
    +                    jsr223PostProcessor("props.put('MY_TEST_TOKEN', prev.responseDataAsString)")
    +                )
    +        ),
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/products")
    +                .header("X-MY-TOKEN", "${__P(MY_TEST_TOKEN)}")
    +        ),
    +        teardownThreadGroup(
    +            httpSampler("http://my.service/tokens/${__P(MY_TEST_TOKEN)}")
    +                .method(HTTPConstants.DELETE)
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    By default, JMeter automatically executes teardown thread groups when a test plan stops due to an unscheduled event like a sample error when a stop test action is configured in a thread group, invocation of ctx.getEngine().askThreadsToStop() in jsr223 element, etc. You can disable this behavior by using the testPlan tearDownOnlyAfterMainThreadsDone method, which might be helpful if the teardown thread group has only to run on clean test plan completion.

    Check DslSetupThreadGroupopen in new window and DslTeardownThreadGroupopen in new window for additional tips and details on the usage of these components.

    Thread groups order

    By default, when you add multiple thread groups to a test plan, JMeter will run them all in parallel. This is a very helpful behavior in many cases, but in some others, you may want to run them sequentially (one after the other). To achieve this you can just use sequentialThreadGroups() test plan method.

    Test plan debugging

    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-java-dsl you have several options for this purpose.

    View results tree

    One option is using provided resultsTreeVisualizer() like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("http://my.service")
    +        ),
    +        resultsTreeVisualizer()
    +    ).run();
    +  }
    +
    +}
    +

    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:

    View Results Tree GUI

    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 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.

    TIP

    Remove 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.

    WARNING

    By default, View Results Tree only displays the last 500 sample results. If you need to display more elements, use provided 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.

    Post-processor breakpoints

    Another alternative is using IDE's built-in debugger by adding a jsr223PostProcessor with java code and adding a breakpoint to the post-processor code. This does not only allow checking sample result information but also JMeter variables and properties values and sampler properties.

    Here is an example screenshot using this approach while debugging with an IDE:

    Post Processor debugging in IDE

    TIP

    DSL provides following methods to ease results and variables visualization and debugging: varsMap(), prevMap(), prevMetadata(), prevMetrics(), prevRequest(), prevResponse(). Check PostProcessorVarsopen in new window and Jsr223ScriptVarsopen in new window for more details.

    TIP

    Remove such post processors when no longer needed (when debugging is finished). Leaving them would generate errors when loading generated JMX test plan or running the test plan in BlazeMeter, OctoPerf or Azure, in addition to unnecessary processing time and resource usage.

    Debug info during test plan execution

    Another option that allows collecting debugging information during a test plan execution without affecting test plan execution (doesn't stop the test plan on each breakpoint as IDE debugger does, which will affect test plan collected metrics) and allows analyzing information after test plan execution, is using debugPostProcessor which adds a sub result to sampler results including debug information.

    Here is an example that collects JMeter variables that can be reviewed with included resultsTreeVisualizer:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    String userIdVarName = "USER_ID";
    +    String usersPath = "/users";
    +    testPlan(
    +        httpDefaults().url("http://my.service"),
    +        threadGroup(1, 1,
    +            httpSampler(usersPath)
    +                .children(
    +                    jsonExtractor(userIdVarName, "[].id"),
    +                    debugPostProcessor()
    +                ),
    +            httpSampler(usersPath + "/${" + userIdVarName + "}")
    +        ),
    +        resultsTreeVisualizer()
    +    ).run();
    +  }
    +
    +}
    +

    This approach is particularly helpful when debugging extractors, allowing you to see what JMeter variables were or were not generated by previous extractors.

    In general, prefer using Post processor with IDE debugger breakpoint in the initial stages of test plan development, testing with just 1 thread in thread groups, and using this later approach when trying to debug issues that are reproducible only in multiple threads executions or in a particular environment that requires offline analysis (analyze collected information after test plan execution).

    TIP

    Use this element in combination with resultsTreeVisualizer to review live executions, or use jtlWriter with withAllFields() or saveAsXml(true) and saveResponseData(true) to generate a jtl file for later analysis.

    TIP

    By default, debugPostProcessor will only include JMeter variables in generated sub sampler, which covers the most used case and keeps memory and disk usage low. debugPostProcessor includes additional methods that allow including other information like sampler properties, JMeter properties, and system properties. Check DslDebugPostProcessoropen in new window for more details.

    Debug JMeter code

    You can even add breakpoints to JMeter code in your IDE and debug the code line by line providing the greatest possible detail.

    Here is an example screenshot debugging HTTP Sampler:

    JMeter HTTP Sampler debugging in IDE

    TIP

    JMeter class in charge of executing threads logic is org.apache.jmeter.threads.JMeterThread. You can check the classes used by each DSL-provided test element by checking the DSL code.

    Debug Groovy code

    In some cases, you may want to debug some Groovy script used in some sampler, pre-, or post-processor. For such scenarios, you can check hereopen in new window where we list some options.

    Dummy sampler

    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 dummySampler (which uses Dummy Sampler pluginopen in new window) to be able to test extractors, assertions, controllers conditions, and other parts of the test plan under certain conditions/results generated by the samplers.

    Here is an example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    String usersIdVarName = "USER_IDS";
    +    String userIdVarName = "USER_ID";
    +    String usersPath = "/users";
    +    testPlan(
    +        httpDefaults().url("http://my.service"),
    +        threadGroup(1, 1,
    +            // httpSampler(usersPath)
    +            dummySampler("[{\"id\": 1, \"name\": \"John\"}, {\"id\": 2, \"name\": \"Jane\"}]")
    +                .children(
    +                    jsonExtractor(usersIdVarName, "[].id")
    +                        .matchNumber(-1)
    +                ),
    +            forEachController(usersIdVarName, userIdVarName,
    +                // httpSampler(usersPath + "/${" + userIdVarName + "}")
    +                dummySampler("{\"name\": \"John or Jane\"}")
    +                    .url("http://my.service/" + usersPath + "/${" + userIdVarName + "}")
    +            )
    +        ),
    +        resultsTreeVisualizer()
    +    ).run();
    +  }
    +
    +}
    +

    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 responseTimeSimulation() method.

    Check DslDummySampleropen in new window for more information o additional configuration and options.

    Test plan review in JMeter GUI

    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 showInGui() method in a test plan to open JMeter GUI with the preloaded test plan.

    This can be also used to debug the test plan, by adding elements (like view results tree, dummy samplers, debug post-processors, etc.) in the GUI and running the test plan.

    Here is a simple example using the method:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        )
    +    ).showInGui();
    +  }
    +
    +}
    +

    Which ends up opening a window like this one:

    Test plan in JMeter GUI

    Reporting

    Once you have a test plan you would usually want to be able to analyze the collected information. This section contains several ways to achieve this.

    Log requests and responses

    The main mechanism provided by JMeter (and jmeter-java-dsl) to get information about generated requests, responses, and associated metrics is through the generation of JTL files.

    This can be easily achieved in jmeter-java-dsl by using provided jtlWriter like in this example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import java.time.Instant;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        jtlWriter("target/jtls")
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    By default, 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 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 JtlWriteropen in new window for more details.

    TIP

    By default, jtlWriter will log every sample result, but in some cases you might want to log additional info when a sample result fails. In such scenarios you can use two jtlWriter instances like in this example:

    testPlan(
    +    threadGroup(2, 10,
    +        httpSampler("http://my.service")
    +    ),
    +    jtlWriter("target/jtls/success")
    +      .logOnly(SampleStatus.SUCCESS),
    +    jtlWriter("target/jtls/error")
    +      .logOnly(SampleStatus.ERROR)
    +      .withAllFields(true)
    +)
    +

    TIP

    jtlWriter will automatically generate .jtl files applying this format: <yyyy-MM-dd HH-mm-ss> <UUID>.jtl.

    If you need a specific file name, for example for later postprocessing logic (eg: using CI build ID), you can specify it by using jtlWriter(directory, fileName).

    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 responseFileSaver which automatically generates a file for each received response. Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import java.time.Instant;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        responseFileSaver(Instant.now().toString().replace(":", "-") + "-response")
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    Check ResponseFileSaveropen in new window for more details.

    Finally, if you have more specific needs that are not covered by previous examples, you can use jsr223PostProcessor to define your own custom logic like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import java.time.Instant;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +                .children(jsr223PostProcessor(
    +                    "new File('traceFile') << \"${prev.sampleLabel}>>${prev.responseDataAsString}\\n\""))
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    Check DslJsr223PostProcessoropen in new window for more details.

    Real-time metrics visualization and historic data storage

    When running tests with JMeter (and in particular with jmeter-java-dsl) a usual requirement is to be able to store such test runs in a persistent database to, later on, review such metrics, and compare different test runs. Additionally, jmeter-java-dsl only provides some summary data of a test run in the console while it is running, but, since it doesn't provide any sort of UI, this doesn't allow you to easily analyze such information as it can be done in JMeter GUI.

    InfluxDB

    To overcome these limitations you can use provided support for publishing JMeter test run metrics to InfluxDBopen in new window or Elasticsearchopen in new window, which allows keeping a record of all run statistics and, through Grafanaopen in new window, get some nice dashboards like the following one:

    Grafana JMeter Dashboard Example

    This can be easily done using influxDbListener, an existing InfluxDB & Grafana server, and using a dashboard like this oneopen in new window.

    Here is an example test plan:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        influxDbListener("http://localhost:8086/write?db=jmeter")
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    If you want to try it locally, you can run docker-compose up (previously installing Dockeropen in new window in your machine) inside this directoryopen in new window. After containers are started, you can open Grafana at http://localhost:3000open in new window. Finally, run a performance test using the influxDbListener and you will be able to see the live results, and keep historic data. Cool, isn't it?!

    WARNING

    Use the provided docker-compose settings for local tests only. It uses weak credentials and is not properly configured for production purposes.

    Check InfluxDbBackendListeneropen in new window for additional details and settings.

    Graphite

    In a similar fashion to InfluxDB, you can use Graphite and Grafana. Here is an example test plan using the graphiteListener:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        graphiteListener("localhost:2004")
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    As in the InfluxDB scenario, you can try it locally by running docker-compose up (previously installing Dockeropen in new window in your machine) inside this directoryopen in new window. After containers are started, you can follow the same steps as in the InfluxDB scenario.

    WARNING

    Use the provided docker-compose settings for local tests only. It uses weak credentials and is not properly configured for production purposes.

    WARNING

    graphiteListener is configured to use Pickle Protocol, and port 2004, by default. This is more efficient than text plain protocol, which is the one used by default by JMeter.

    Elasticsearch

    Another alternative is using provided jmeter-java-dsl-elasticsearch-listener module with Elasticsearch and Grafana servers using a dashboard like this oneopen in new window.

    To use the module, you will need to include the following dependency in your project:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-elasticsearch-listener</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    repositories {
    +    ...
    +    maven { url 'https://jitpack.io' }
    +}
    +
    +dependencies {
    +    ...
    +    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-elasticsearch-listener:1.29'
    +}
    +

    And use provided elasticsearchListener() method like in this example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.elasticsearch.listener.ElasticsearchBackendListener.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        elasticsearchListener("http://localhost:9200/jmeter")
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    WARNING

    This module uses this JMeter pluginopen in new window which, at its current version, has performance and dependency issues that might affect your project. Thisopen in new window and thisopen in new window pull requests fix those issues, but until they are merged and released, you might face such issues.

    In the same fashion as InfluxDB, if you want to try it locally, you can run docker-compose up inside this directoryopen in new window and follow similar steps as described for InfluxDB to visualize live metrics in Grafana.

    WARNING

    Use provided docker-compose settings for local tests only. It uses weak or no credentials and is not properly configured for production purposes.

    Check ElasticsearchBackendListeneropen in new window for additional details and settings.

    Prometheus

    As in previous scenarios, you can also use Prometheus and Grafana.

    To use the module, you will need to include the following dependency in your project:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-prometheus</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-prometheus:1.29'
    +

    And use provided prometheusListener() method like in this example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.prometheus.DslPrometheusListener.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        prometheusListener()
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    As in previous cases, you can to try it locally by running docker-compose up inside this directoryopen in new window. After containers are started, you can follow the same steps as in previous scenarios.

    WARNING

    Use the provided docker-compose settings for local tests only. It uses weak credentials and is not properly configured for production purposes.

    Check DslPrometheusListeneropen in new window for details on listener settings.

    Here is an example that shows the default settings used by prometheusListener:

    import us.abstracta.jmeter.javadsl.prometheus.DslPrometheusListener.PrometheusMetric;
    +...
    +prometheusListener()
    +  .metrics(
    +    PrometheusMetric.responseTime("ResponseTime", "the response time of samplers")
    +      .labels(PrometheusMetric.SAMPLE_LABEL, PrometheusMetric.RESPONSE_CODE)
    +      .quantile(0.75, 0.5)
    +      .quantile(0.95, 0.1)
    +      .quantile(0.99, 0.01)
    +      .maxAge(Duration.ofMinutes(1)),
    +    PrometheusMetric.successRatio("Ratio", "the success ratio of samplers")
    +      .labels(PrometheusMetric.SAMPLE_LABEL, PrometheusMetric.RESPONSE_CODE)
    +  )
    +  .port(9270)
    +  .host("0.0.0.0")
    +  .endWait(Duration.ofSeconds(10))
    +...
    +

    Note that the default settings are different from the used JMeter Prometheus Plugin, to allow easier usage and avoid missing metrics at the end of test plan execution.

    TIP

    When configuring the prometheusListener always consider setting a endWait that is greater thant the Prometheus Server configured scrape_interval to avoid missing metrics at the end of test plan execution (e.g.: 2x the scrape interval value).

    DataDog

    Another option is using jmeter-java-dsl-datadog module which uses existing jmeter-datadog-backend-listener pluginopen in new window to upload metrics to datadog which you can easily visualize and analize with DataDog provided JMeter dashboardopen in new window. Here is an example of what you get:

    datadog jmeter dashboard

    To use the module, just include the dependency:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-datadog</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    repositories {
    +    ...
    +    maven { url 'https://jitpack.io' }
    +}
    +
    +dependencies {
    +    ...
    +    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-datadog:1.29'
    +}
    +

    And use provided datadogListener() method like in this example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.datadog.DatadogBackendListener.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        datadogBackendListener(System.getenv("DATADOG_APIKEY"))
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    If you use a DataDog instance in a site different than US1 (the default one), you can use .site(DatadogSite) method to select the proper site.

    TIP

    You can use .resultsLogs(true) to send results samples as logs to DataDog to get more information in DataDog on each sample of the test plan (for example for tracing). Enabling this property requires additional network traffic, that may affect test plan execution, and costs on DataDog, so use it sparingly.

    TIP

    You can use .tags() to add additional information to metrics sent to DataDog. Check DataDog documentationopen in new window for more details.

    Generate HTML reports from test plan execution

    After running a test plan you would usually like to visualize the results in a friendly way that eases the analysis of collected information.

    One, and preferred way, to do that is through previously mentioned alternatives.

    Another way might just be using previously introduced jtlWriter and then loading the jtl file in JMeter GUI with one of JMeter provided listeners (like view results tree, summary report, etc.).

    Another alternative is generating a standalone report for the test plan execution using jmeter-java-dsl provided htmlReporter like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import java.time.Instant;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        ),
    +        htmlReporter("reports")
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    WARNING

    htmlReporter will create one directory for each generated report by applying the following template: <yyyy-MM-dd HH-mm-ss> <UUID>.

    If you need a particular name for the report directory, for example for postprocessing logic (eg: adding CI build ID), you can use htmlReporter(reportsDirectory, name) to specify the name.

    Make sure when specifying the name, for it to be unique, otherwise report generation will fail after test plan execution.

    TIP

    Time graphs by default group metrics per minute, but you can change this with provided timeGraphsGranularity method.

    Live built-in graphs and stats

    Sometimes you want to get live statistics on the test plan and don't want to install additional tools, and are not concerned about keeping historic data.

    You can use dashboardVisualizer to get live charts and stats for quick review.

    To use it, you need to add the following dependency:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-dashboard</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-dashboard:1.29'
    +

    And use it as you would with any of the previously mentioned listeners (like influxDbListener and jtlWriter).

    Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.dashboard.DashboardVisualizer.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup("Group1")
    +            .rampToAndHold(10, Duration.ofSeconds(10), Duration.ofSeconds(10))
    +            .children(
    +                httpSampler("Sample 1", "http://my.service")
    +            ),
    +        threadGroup("Group2")
    +            .rampToAndHold(20, Duration.ofSeconds(10), Duration.ofSeconds(20))
    +            .children(
    +                httpSampler("Sample 2", "http://my.service/get")
    +            ),
    +        dashboardVisualizer()
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    The dashboardVisualizer will pop up a window like the following one, which you can use to trace statistics while the test plan runs:

    Dashboard example window

    WARNING

    The dashboard imposes additional resources (CPU & RAM) consumption on the machine generating the load test, which may affect the test plan execution and reduce the number of concurrent threads you may reach in your machine. In general, prefer using one of the previously mentioned methods and using the dashboard just for local testing and quick feedback.

    Remember to remove it when is no longer needed in the test plan

    WARNING

    The test will not end until you close all popup windows. This allows you to see the final charts and statistics of the plan before ending the test.

    TIP

    As with jtlWriter and influxDbListener, you can place dashboardVisualizer at different levels of the test plan (at the test plan level, at the thread group level, as a child of a sampler, etc.), to only capture statistics of that particular part of the test plan.

    Response processing

    Check for expected response

    By default, JMeter marks any HTTP request with a fail response code (4xx or 5xx) as failed, which allows you to easily identify when some request unexpectedly fails. But in many cases, this is not enough or desirable, and you need to check for the response body (or some other field) to contain (or not) a certain string.

    This is usually accomplished in JMeter with the usage of Response Assertions, which provides an easy and fast way to verify that you get the proper response for each step of the test plan, marking the request as a failure when the specified condition is not met.

    Here is an example of how to specify a response assertion in jmeter-java-dsl:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +                .children(
    +                    responseAssertion().containsSubstrings("OK")
    +                )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    Check Response Assertionopen in new window for more details and additional options.

    For more complex scenarios check following section.

    Check for expected JSON

    When checking for JSON responses, it is usually easier to just use jsonAssertion. Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\"name\": \"John Doe\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    jsonAssertion("id")
    +                ),
    +            httpSampler("http://my.service/accounts/${ACCOUNT_ID}")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    Previous example just checks that sample result JSON contains an id field. You can use matches(regex), equalsTo(value) or even equalsToJson(json) methods to check id associated value. Additionally, you can use the not() method to check for the inverse condition. E.g.: does not contain id field, or field value does not match a given regular expression or is not equal to a given value.

    TIP

    By default this element uses JMeter JSON JMESPathopen in new window Assertion element, and in consequence, JMESPath as query language.

    If you want to use JMeter JSON Assertion element, and in consequence JSONPathopen in new window as the query language, you can simply use .queryLanguage(JsonQueryLanguage.JSON_PATH) and a JSONPath query.

    Change sample result statuses with custom logic

    Sometimes response assertions and JMeter default behavior are not enough, and custom logic is required. In such scenarios you can use jsr223PostProcessor as in this example where the 429 status code is not considered as a fail status code:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +                .children(
    +                    jsr223PostProcessor(
    +                        "if (prev.responseCode == '429') { prev.successful = true }")
    +                )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    You can also use a Java lambda instead of providing Groovy script, which benefits from Java type safety & IDEs code auto-completion and consumes less CPU:

    jsr223PostProcessor(s -> {
    +    if ("429".equals(s.prev.getResponseCode())) {
    +      s.prev.setSuccessful(true);
    +    }
    +})
    +

    WARNING

    Even though using Java Lambdas has several benefits, they are also less portable. Check following section for more details.

    Check DslJsr223PostProcessoropen in new window for more details and additional options.

    WARNING

    JSR223PostProcessor is a very powerful tool but is not the only, nor the best, alternative for many cases where JMeter already provides a better and simpler alternative. For instance, the previous example might be implemented with previously presented Response Assertion.

    Lambdas

    As previously mentioned, using Java lambdas is in general more performant than using Groovy scripts (hereopen in new window are some comparisons) and are easier to develop and maintain due to type safety, IDE autocompletion, etc.

    But, they are also less portable.

    For instance, they will not work out of the box with remote engines (like BlazeMeterEngine) or while saving JMX and running it in standalone JMeter.

    One option is using groovy scripts and __groovy function, but doing so, you lose the previously mentioned benefits.

    Here is another approach to still benefit from Java code (vs Groovy script) and run in remote engines and standalone JMeter.

    Lambdas in remote engine

    Here are the steps to run test plans containing Java lambdas in BlazeMeterEngine:

    1. Replace all Java lambdas with public static classes implementing proper script interface.

      For example, if you have the following test:

      public class PerformanceTest {
      +
      +  @Test
      +  public void testPerformance() throws Exception {
      +    testPlan(
      +        threadGroup(2, 10,
      +            httpSampler("http://my.service")
      +                .children(
      +                    jsr223PostProcessor(s -> {
      +                      if ("429".equals(s.prev.getResponseCode())) {
      +                        s.prev.setSuccessful(true);
      +                      }
      +                    })
      +                )
      +        )
      +    ).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN")));
      +  }
      +  
      +}
      +

      You can change it to:

      public class PerformanceTest {
      +  
      +  public static class StatusSuccessProcessor implements PostProcessorScript {
      +
      +    @Override
      +    public void runScript(PostProcessorVars s) {
      +      if ("429".equals(s.prev.getResponseCode())) {
      +        s.prev.setSuccessful(true);
      +      }
      +    }
      +    
      +  }
      +
      +  @Test
      +  public void testPerformance() throws Exception {
      +    testPlan(
      +        threadGroup(2, 10,
      +            httpSampler("http://my.service")
      +                .children(
      +                    jsr223PostProcessor(StatusSuccessProcessor.class)
      +                )
      +        )
      +    ).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN")));
      +  }
      +
      +}
      +

      Script interface to implement, depends on where you use the lambda code. Available interfaces are PropertyScript, PreProcessorScript, PostProcessorScript, and SamplerScript.

    2. Upload your test code and dependencies to BlazeMeter.

      If you use maven, here is what you can add to your project to configure this:

      <plugins>
      +  ...
      +  <!-- this generates a jar containing your test code (including the public static class previously mentioned) -->
      +  <plugin>
      +     <groupId>org.apache.maven.plugins</groupId>
      +     <artifactId>maven-jar-plugin</artifactId>
      +     <version>3.3.0</version>
      +     <executions>
      +       <execution>
      +         <goals>
      +           <goal>test-jar</goal>
      +         </goals>
      +       </execution>
      +     </executions>
      +  </plugin>
      +  <!-- this copies project dependencies to target/libs directory -->
      +  <plugin>
      +    <groupId>org.apache.maven.plugins</groupId>
      +    <artifactId>maven-dependency-plugin</artifactId>
      +    <version>3.6.0</version>
      +    <executions>
      +      <execution>
      +        <id>copy-dependencies</id>
      +        <phase>package</phase>
      +        <goals>
      +          <goal>copy-dependencies</goal>
      +        </goals>
      +        <configuration>
      +          <outputDirectory>${project.build.directory}/libs</outputDirectory>
      +          <!-- include here, separating by commas, any additional dependencies (just the artifacts ids) you need to upload to BlazeMeter -->
      +          <!-- AzureEngine automatically uploads JMeter dsl artifacts, so only transitive or custom dependencies would be required -->
      +          <!-- if you would like for BlazeMeterEngine and OctoPerfEngine to automatically upload JMeter DSL artifacts, please create an issue in GitHub repository -->
      +          <includeArtifactIds>jmeter-java-dsl</includeArtifactIds>
      +        </configuration>
      +      </execution>
      +    </executions>
      +  </plugin>
      +  <!-- this takes care of executing tests classes ending with IT after test jar is generated and dependencies are copied -->
      +  <!-- additionally, it sets some system properties as to easily identify test jar file -->
      +  <plugin>
      +    <groupId>org.apache.maven.plugins</groupId>
      +    <artifactId>maven-failsafe-plugin</artifactId>
      +    <version>3.0.0-M7</version>
      +    <configuration>
      +      <systemPropertyVariables>
      +        <testJar.path>${project.build.directory}/${project.artifactId}-${project.version}-tests.jar</testJar.path> 
      +      </systemPropertyVariables>
      +    </configuration>
      +    <executions>
      +      <execution>
      +        <goals>
      +          <goal>integration-test</goal>
      +          <goal>verify</goal>
      +        </goals>
      +      </execution>
      +    </executions>
      +  </plugin>
      +</plugins>
      +

      Additionally, rename your test class to use IT suffix (so it runs after test jar is created and dependencies are copied), and add to BlazeMeterEngine logic to upload the jars. For example:

      // Here we renamed from PerformanceTest to PerformanceIT
      +public class PerformanceIT {
      +  
      +   ...
      +   
      +   @Test
      +   public void testPerformance() throws Exception {
      +      testPlan(
      +              ...
      +      ).runIn(new BlazeMeterEngine(System.getenv("BZ_TOKEN"))
      +              .assets(findAssets()));
      +   }
      +
      +   private File[] findAssets() {
      +      File[] libsFiles = new File("target/libs").listFiles();
      +      File[] ret = new File[libsFiles.length + 1];
      +      ret[0] = new File(System.getProperty("testJar.path"));
      +      System.arraycopy(libsFiles, 0, ret, 1, libsFiles.length);
      +      return ret;
      +   }
      +
      +}
      +

      TIP

      Currently only BlazeMeterEngine and AzureEngine provide a way to upload assets. If you need support for other engines, please request it in an issueopen in new window.

    3. Execute your tests with maven (either with mvn clean verify or as part of mvn clean install) or IDE (by first packaging your project, and then executing PerformanceIT test).

    Lambdas in standalone JMeter

    If you save your test plan with the saveAsJmx() test plan method and then want to execute the test plan in JMeter, you will need to:

    1. Replace all Java lambdas with public static classes implementing proper script interface.

      Same as the previous section.

    2. Package your test code in a jar.

      Same as the previous section.

    3. Copy all dependencies, in addition to jmeter-java-dsl, required by the lambda code to JMeter lib/ext folder.

      You can also use maven-dependency-plugin and run mvn package -DskipTests to get the actual jars. If the test plan requires any particular jmeter plugin, then you would need to copy those as well.

    Use part of a response in a subsequent request (aka correlation)

    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.

    Regular expressions extraction

    Here is an example with jmeter-java-dsl using regular expressions:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\"name\": \"John Doe\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    regexExtractor("ACCOUNT_ID", "\"id\":\"([^\"]+)\"")
    +                ),
    +            httpSampler("http://my.service/accounts/${ACCOUNT_ID}")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    Check DslRegexExtractoropen in new window for more details and additional options.

    Boundaries based extraction

    Regular expressions are quite powerful and flexible, but also are complex and performance might not be optimal in some scenarios. When you know that desired extraction is always surrounded by some specific text that never varies, then you can use boundaryExtractor which is simpler and in many cases more performant:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\"name\": \"John Doe\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    boundaryExtractor("ACCOUNT_ID", "\"id\":\"", "\"")
    +                ),
    +            httpSampler("http://my.service/accounts/${ACCOUNT_ID}")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    Check DslBoundaryExtractoropen in new window for more details and additional options.

    JSON extraction

    When the response of a request is JSON, then you can use jsonExtractor like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\"name\": \"John Doe\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    jsonExtractor("ACCOUNT_ID", "id")
    +                ),
    +            httpSampler("http://my.service/accounts/${ACCOUNT_ID}")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    By default this element uses JMeter JSON JMESPathopen in new window Extractor element, and in consequence JMESPath as query language.

    If you want to use JMeter JSON Extractor element, and in consequence JSONPathopen in new window as query language, you can simply use .queryLanguage(JsonQueryLanguage.JSON_PATH) and a JSONPath query.

    Requests generation

    Conditionals

    At some point, you will need to execute part of a test plan according to a certain condition (eg: a value extracted from a previous request). When you reach that point, you can use ifController like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/accounts")
    +                .post("{\"name\": \"John Doe\"}", ContentType.APPLICATION_JSON)
    +                .children(
    +                    regexExtractor("ACCOUNT_ID", "\"id\":\"([^\"]+)\"")
    +                ),
    +            ifController("${__groovy(vars['ACCOUNT_ID'] != null)}",
    +                httpSampler("http://my.service/accounts/${ACCOUNT_ID}")
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    You can also use a Java lambda instead of providing JMeter expression, which benefits from Java type safety & IDEs code auto-completion and consumes less CPU:

    ifController(s -> s.vars.get("ACCOUNT_ID") != null,
    +    httpSampler("http://my.service/accounts/${ACCOUNT_ID}")
    +)
    +

    WARNING

    Even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    Check DslIfControlleropen in new window and JMeter Component documentationopen in new window for more details.

    Loops

    Iterating over extracted values

    A common use case is to iterate over a list of values extracted from a previous request and execute part of the plan for each extracted value. This can be easily done using foreachController like in the following example:

    package us.abstracta.jmeter.javadsl;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    String productsIdVarName = "PRODUCT_IDS";
    +    String productIdVarName = "PRODUCT_ID";
    +    String productsPath = "/products";
    +    TestPlanStats stats = testPlan(
    +        httpDefaults().url("http://my.service"),
    +        threadGroup(2, 10,
    +            httpSampler(productsPath)
    +                .children(
    +                    jsonExtractor(productsIdVarName, "[].id")
    +                        .matchNumber(-1)
    +                ),
    +            forEachController(productsIdVarName, productIdVarName,
    +                httpSampler(productsPath + "/${" + productIdVarName + "}")
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    JMeter automatically generates a variable __jm__<loopName>__idx with the current index of the for each iteration (starting with 0), which you can use in controller children elements if needed. The default name for the for each controller, when not specified, is foreach.

    Check DslForEachControlleropen in new window for more details.

    Iterating while a condition is met

    If at any time you want to execute a given part of a test plan, inside a thread iteration, while a condition is met, then you can use whileController (internally using JMeter While Controlleropen in new window) like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            whileController("${__groovy(vars['ACCOUNT_ID'] == null)}",
    +                httpSampler("http://my.service/accounts")
    +                    .post("{\"name\": \"John Doe\"}", ContentType.APPLICATION_JSON)
    +                    .children(
    +                        regexExtractor("ACCOUNT_ID", "\"id\":\"([^\"]+)\"")
    +                    )
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    As with ifController, you can also use Java lambdas to benefit from IDE auto-completion and type safety and less CPU consumption. Eg:

    whileController(s -> s.vars.get("ACCOUNT_ID") == null,
    +    httpSampler("http://my.service/accounts")
    +      .post("{\"name\": \"John Doe\"}", Type.APPLICATION_JSON)
    +      .children(
    +        regexExtractor("ACCOUNT_ID", "\"id\":\"([^\"]+)\"")
    +      )
    +)
    +

    WARNING

    Even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    WARNING

    JMeter evaluates while conditions before entering each iteration, and after exiting each iteration. Take this into consideration if the condition has side effects (eg: incrementing counters, altering some other state, etc).

    TIP

    JMeter automatically generates a variable __jm__<loopName>__idx with the current index of while iteration (starting with 0). Example:

    whileController("items", "${__groovy(vars.getObject('__jm__items__idx') < 4)}",
    +    httpSampler("http://my.service/items")
    +      .post("{\"name\": \"My Item\"}", Type.APPLICATION_JSON)
    +)
    +

    The default name for the while controller, when not specified, is while.

    Check DslWhileControlleropen in new window for more details.

    Iterating a fixed number of times

    In simple scenarios where you just want to execute a fixed number of times, within a thread group iteration, a given part of the test plan, you can just use forLoopController (which uses JMeter Loop Controller componentopen in new window) as in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            forLoopController(5,
    +                httpSampler("http://my.service/accounts")
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    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 __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.

    Check ForLoopControlleropen in new window for more details.

    Iterating for a given period

    In some scenarios you might want to execute a given logic until all the steps are executed or a given period of time has passed. In these scenarios you can use runtimeController which stops executing children elements when a specified time is reached.

    Here is an example which makes requests to a page until token expires by using runtimeController in combination with whileController.

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    Duration tokenExpiration = Duration.ofSeconds(5);
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/token"),
    +            runtimeController(tokenExpiration,
    +                whileController("true",
    +                    httpSampler("http://my.service/accounts")
    +                )
    +            )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    Check DslRuntimeControlleropen in new window for more details.

    Execute only once in thread

    In some cases, you only need to run part of a test plan once. For these need, you can use onceOnlyController. This controller will execute a part of the test plan only one time on the first iteration of each thread (using JMeter Once Only Controller Componentopen in new window).

    You can use this, for example, for one-time authorization or for setting JMeter variables or properties.

    Here is an example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.JmeterDslTest;
    +
    +public class DslOnceOnlyControllerTest extends JmeterDslTest {
    +
    +  @Test
    +  public void shouldExecuteOnlyOneTimeWhenOnceOnlyControllerInPlan() throws Exception {
    +    testPlan(
    +        threadGroup(1, 10,
    +            onceOnlyController(
    +                httpSampler("http://my.service/login") // only runs once
    +                    .method(HTTPConstants.POST)
    +                    .header("Authorization", "Basic asdf=")
    +                    .children(
    +                        regexExtractor("AUTH_TOKEN", "authToken=(.*)")
    +                    )
    +            ),
    +            httpSampler("http://my.service/accounts") // runs ten times
    +                .header("Authorization", "Bearer ${AUTH_TOKEN}")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Check DslOnceOnlyControlleropen in new window for more details.

    Group requests

    Sometimes, is necessary to be able to group requests which constitute different steps in a test. For example, to separate necessary requests to do a login from the ones used to add items to the cart and the ones to do a purchase. JMeter (and the DSL) provide Transaction Controllers for this purpose, here is an example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testTransactions() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            transaction('login',
    +                httpSampler("http://my.service"),
    +                httpSampler("http://my.service/login")
    +                    .post("user=test&password=test", ContentType.APPLICATION_FORM_URLENCODED)
    +            ),
    +            transaction('addItemToCart',
    +                httpSampler("http://my.service/items"),
    +                httpSampler("http://my.service/cart/items")
    +                    .post("{\"id\": 1}", ContentType.APPLICATION_JSON)
    +            )
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    This will provide additional sample results for each transaction, which contain the aggregate metrics for containing requests, allowing you to focus on the actual flow steps instead of each particular request.

    If you don't want to generate additional sample results (and statistics), and want to group requests for example to apply a given timer, config, assertion, listener, pre- or post-processor, then you can use simpleController like in following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testTransactions() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            simpleController('login',
    +                httpSampler("http://my.service"),
    +                httpSampler("http://my.service/users"),
    +                responseAssertion()
    +                  .containsSubstrings("OK")
    +            )
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    You can even use transactionController and simpleController to easily modularize parts of your test plan into Java methods (or classes) like in this example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.controllers.DslTransactionController;
    +
    +public class PerformanceTest {
    +
    +  private DslTransactionController login(String baseUrl) {
    +    return transaction("login",
    +        httpSampler(baseUrl),
    +        httpSampler(baseUrl + "/login")
    +            .post("user=test&password=test", ContentType.APPLICATION_FORM_URLENCODED)
    +    );
    +  }
    +
    +  private DslTransactionController addItemToCart(String baseUrl) {
    +    return transaction("addItemToCart",
    +        httpSampler(baseUrl + "/items"),
    +        httpSampler(baseUrl + "/cart/items")
    +            .post("{\"id\": 1}", ContentType.APPLICATION_JSON)
    +    );
    +  }
    +
    +  @Test
    +  public void testTransactions() throws IOException {
    +    String baseUrl = "http://my.service";
    +    testPlan(
    +        threadGroup(2, 10,
    +            login(baseUrl),
    +            addItemToCart(baseUrl)
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    CSV as input data for requests

    Sometimes is necessary to run the same flow but using different pre-defined data on each request. For example, a common use case is to use a different user (from a given set) in each request.

    This can be easily achieved using the provided csvDataSet element. For example, having a file like this one:

    USER,PASS
    +user1,pass1
    +user2,pass2
    +

    You can implement a test plan that tests recurrent login with the two users with something like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        csvDataSet("users.csv"),
    +        threadGroup(5, 10,
    +            httpSampler("http://my.service/login")
    +                .post("{\"${USER}\": \"${PASS}\"", ContentType.APPLICATION_JSON),
    +            httpSampler("http://my.service/logout")
    +                .method(HTTPConstants.POST)
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    To properly format the data in your CSV, a general rule you can apply is to replace each double quotes with two double quotes and add double quotes to the beginning and end of each CSV value.

    E.g.: if you want one CSV field to contain the value {"field": "value"}, then use "{""field:"": ""value""}".

    This way, with a simple search and replace, you can include in a CSV field any format like JSON, XML, etc.

    Note: JMeter uses should be aware that JMeter DSL csvDataSet sets Allowed quoted data? flag, in associated Csv Data Set Config element, to true.

    By default, the CSV file will be opened once and shared by all threads. This means that when one thread reads a CSV line in one iteration, then the following thread reading a line will continue with the following line.

    If you want to change this (to share the file per thread group or use one file per thread), then you can use the provided sharedIn method like in the following example:

    import us.abstracta.jmeter.javadsl.core.configs.DslCsvDataSet.Sharing;
    +...
    +  TestPlanStats stats = testPlan(
    +      csvDataSet("users.csv")
    +        .sharedIn(Sharing.THREAD),
    +      threadGroup(5, 10,
    +          httpSampler("http://my.service/login")
    +            .post("{\"${USER}\": \"${PASS}\"", Type.APPLICATION_JSON),
    +          httpSampler("http://my.service/logout")
    +            .method(HTTPConstants.POST)
    +      )
    +  )
    +

    :::

    WARNING

    You can use the randomOrder() method to get CSV lines in random order (using Random CSV Data Set pluginopen in new window), but this is less performant as getting them sequentially, so use it sparingly.

    Check DslCsvDataSetopen in new window for additional details and options (like changing delimiter, handling files without headers line, stopping on the end of file, etc.).

    Counter

    In scenarios that you need unique value for each request, for example for id parameters, you can use counter which provides easy means to have an auto incremental value that can be used in requests.

    Here is an example:

    testPlan(
    +    threadGroup(1, 10,
    +        counter("USER_ID")
    +            .startingValue(1000), // will generate 1000, 1001, 1002...
    +        httpSampler(wiremockUri + "/${USER_ID}")
    +    )
    +).run();
    +

    Check DslCounteropen in new window for more details.

    Provide request parameters programmatically per request

    So far we have seen a few ways to generate requests with information extracted from CSV or through a counter, but this is not enough for some scenarios. When you need more flexibility and power you can use jsr223preProcessor to specify your own logic to build each request.

    Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.jmeter.threads.JMeterVariables;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +                .post("${REQUEST_BODY}", ContentType.TEXT_PLAIN)
    +                .children(
    +                    jsr223PreProcessor("vars.put('REQUEST_BODY', " + getClass().getName()
    +                        + ".buildRequestBody(vars))")
    +                )
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +  public static String buildRequestBody(JMeterVariables vars) {
    +    String countVarName = "REQUEST_COUNT";
    +    Integer countVar = (Integer) vars.getObject(countVarName);
    +    int count = countVar != null ? countVar + 1 : 1;
    +    vars.putObject(countVarName, count);
    +    return "MyBody" + count;
    +  }
    +
    +}
    +

    You can also use a Java lambda instead of providing Groovy script, which benefits from Java type safety & IDEs code auto-completion and consumes less CPU:

    jsr223PreProcessor(s -> s.vars.put("REQUEST_BODY", buildRequestBody(s.vars)))
    +

    Or even use this shorthand:

    post(s -> buildRequestBody(s.vars), Type.TEXT_PLAIN)
    +

    WARNING

    Even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    TIP

    jsr223PreProcessor is quite powerful. But, provided example can easily be achieved through the usage of counter element.

    Check DslJsr223PreProcessoropen in new window & DslHttpSampleropen in new window for more details and additional options.

    Timers

    Emulate user delays between requests

    Sometimes, is necessary to be able to properly replicate users' behavior, and in particular the time the users take between sending one request and the following one. For example, to simulate the time it will take to complete a purchase form. JMeter (and the DSL) provide a few alternatives for this.

    If you just want to add 1 pause between two requests, you can use the threadPause method like in the following example:

    import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service/items"),
    +            threadPause(Duration.ofSeconds(4)),
    +            httpSampler("http://my.service/cart/selected-items")
    +                .post("{\"id\": 1}", ContentType.APPLICATION_JSON)
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Using threadPause is a good solution for adding individual pauses, but if you want to add pauses across several requests, or sections of test plan, then using a constantTimer or uniformRandomTimer is better. Here is an example that adds a delay of between 4 and 10 seconds for every request in the test plan:

    import java.io.IOException;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testTransactions() throws IOException {
    +    testPlan(
    +        threadGroup(2, 10,
    +            uniformRandomTimer(Duration.ofSeconds(4), Duration.ofSeconds(10)),
    +            transaction("addItemToCart",
    +                httpSampler("http://my.service/items"),
    +                httpSampler("http://my.service/cart/selected-items")
    +                    .post("{\"id\": 1}", ContentType.APPLICATION_JSON)
    +            ),
    +            transaction("checkout",
    +                httpSampler("http://my.service/cart/chekout"),
    +                httpSampler("http://my.service/cart/checkout/userinfo")
    +                    .post(
    +                        "{\"Name\": Dave, \"lastname\": Tester, \"Street\": 1483  Smith Road, \"City\": Atlanta}",
    +                        ContentType.APPLICATION_JSON)
    +            )
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    TIP

    As you may have noticed, timer order in relation to samplers, doesn't matter. Timers apply to all samplers in their scope, adding a pause after pre-processor executions and before the actual sampling. threadPause order, on the other hand, is relevant, and the pause will only execute when previous samplers in the same scope have run and before following samplers do.

    WARNING

    uniformRandomTimer minimum and maximum parameters differ from the ones used by JMeter Uniform Random Timer element, to make it simpler for users with no JMeter background.

    The generated JMeter test element uses the Constant Delay Offset set to minimum value, and the Maximum random delay set to (maximum - minimum) value.

    Control throughput

    To achieve a specific constant throughput for specific samplers or section of a test plan, you can use throughputTimer, which uses JMeter ConstantThroughputTimer.

    Here is an example for generating a maximum of 120 samples per minute:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    testPlan(
    +        threadGroup(10, Duration.ofSeconds(10),
    +            throughputTimer(120),
    +            httpSampler("http://my.service")
    +        ) 
    +    ).run();
    +  }
    +
    +}
    +

    TIP

    By default, throughtputTimer will control throughput among active threads. If you want to control throughput per thread, i.e. each thread generating the specified throughput, which means that totalThoughput = configuredThroughput * numberOfThreads, you can use perThread() method.

    TIP

    The placement (scope) of the throughputTimer will determine its behaviour. E.g. if you place the timer inside an ifController, it will only control the execution throughput only for elements inside the ifController, or if you place it inside a threadGroup other thread groups execution will be directly not affected (nor they would directly affect this timer execution).

    TIP

    The timer uses by default even distribution of throughput among the active threads. This means that if you have 10 threads and specify 10 tpm, then each thread will try to execute at 1tpm, not adjusting each thread tpm if some other thread was far from achieving the configured tpm. If you want more precise throughput control, you can use .calculation() method, for example, with THREAD_GROUP_ACCURATE, but doing so, may lead to unexpected behavior when using multiple timers in same thread group.

    Check DslThroughputTimeropen in new window for more details.

    WARNING

    throughputTimer works by pausing requests to achieve a constant throughput, so the response times and number of threads must be sufficient to achieve the target throughput. You can think of this timer as a way to limit the maximum throughput, but it does have no way to generate more load if response times are high and threads are not enough. To automatically adjust threads when response times are high you can use rpsThreadGroup as described here.

    WARNING

    On first invocation of throughputTimer on each thread, no delay will be generated by the timer, which may lead to initially higher throughput than expected.

    For example, in previously provided example, 10 requests (1 for each thread) will run without "throughput control", which means you will get 10 requests at once, and after that, you will get 1 request per second (as expected).

    Requests synchronization

    Usually, samples generated by different threads in a test plan thread group start deviating from each other according to the different durations each of them may experience.

    Here is a diagram depicting this behavior, extracted from this nice exampleopen in new window provided by one of JMeter DSL users:

    not synchronized samples in 2 threads and 3 iterations

    In most cases this is ok. But, if you want to generate batches of simultaneous requests to a system under test, this variability will prevent you from getting the expected behavior.

    So, to synchronize requests, by holding some of them until all are in sync, like in this diagram:

    synchronized samples in 2 threads and 3 iterations

    You can use synchronizingTimer like in the following example:

    testPlan(
    +    threadGroup(2, 3,
    +      httpSample("https://mysite"),
    +      synchronizingTimer()
    +    )
    +)
    +

    Execute part of a test plan part a fraction of the times

    In some cases, you may want to execute a given part of the test plan not in every iteration, and only for a given percent of times, to emulate certain probabilistic nature of the flow the users execute.

    In such scenarios, you may use percentController, which uses JMeter Throughput Controller to achieve exactly that.

    Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            percentController(40, // run this 40% of the times
    +                httpSampler("http://my.service/status"),
    +                httpSampler("http://my.service/poll")),
    +            percentController(70, // run this 70% of the times
    +                httpSampler("http://my.service/items"))
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    Check PercentControlleropen in new window for more details.

    Switch between test plan parts with a given probability

    In some cases, you need to switch in a test plan between different behaviors assigning to them different probabilities. The main difference between this need and the previous one is that in each iteration you have to execute one of the parts, while in the previous case you might get multiple or no part executed on a given iteration.

    For this scenario you can use weightedSwitchCotroller, like in this example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            weightedSwitchController()
    +                .child(30, httpSampler("https://myservice/1")) // will run 30/(30+20)=60% of the iterations
    +                .child(20, httpSampler("https://myservice/2")) // will run 20/(30+20)=40% of the iterations
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    DslWeightedSwitchControlleropen in new window for more details.

    Parallel requests

    JMeter provides two main ways for running requests in parallel: thread groups and HTTP samplers downloading embedded resources in parallel. But in some cases is necessary to run requests in parallel which can't be properly modeled with previously mentioned scenarios. For such cases, you can use paralleController which allows using the Parallel Controller pluginopen in new window to execute a given set of requests in parallel (while in a JMeter thread iteration step).

    To use it, add the following dependency to your project:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-parallel</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-dashboard:1.29'
    +

    And use it, like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.parallel.ParallelController.*;
    +
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            parallelController(
    +                httpSampler("http://my.service/status"),
    +                httpSampler("http://my.service/poll"))
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    By default, the controller has no limit on the number of parallel requests per JMeter thread. You can set a limit by using provided maxThreads(int) method. Additionally, you can opt to aggregate children's results in a parent sampler using generateParentSample(boolean) method, in a similar fashion to the transaction controller.

    TIP

    When requesting embedded resources of an HTML response, prefer using downloadEmbeddedResources() method in httpSampler instead. Likewise, when you just need independent parts of a test plan to execute in parallel, prefer using different thread groups for each part.

    Check ParallelControlleropen in new window for additional info.

    JMeter variables & properties

    Variables

    In general, when you want to reuse a certain value of your script, you can, and is the preferred way, just to use Java variables. In some cases though, you might need to pre-initialize some JMeter thread variable (for example to later be used in an ifController) or easily update its value without having to use a jsr223 element for that. For these cases, the DSL provides the vars() method.

    Here is an example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws Exception {
    +    String pageVarName = "PAGE";
    +    String firstPage = "1";
    +    String endPage = "END";
    +    testPlan(
    +        vars()
    +            .set(pageVarName, firstPage),
    +        threadGroup(2, 10,
    +            ifController(s -> !s.vars.get(pageVarName).equals(endPage),
    +                httpSampler("http://my.service/accounts?page=${" + pageVarName +"}")
    +                    .children(
    +                        regexExtractor(pageVarName, "next=.*?page=(\\d+)")
    +                            .defaultValue(endPage)
    +                    )
    +            ),
    +            ifController(s -> s.vars.get(pageVarName).equals(endPage),
    +                vars()
    +                    .set(pageVarName, firstPage)
    +            )
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    WARNING

    For special consideration of existing JMeter users:

    vars() internally uses JMeter User Defined Variables (aka UDV) when placed as a test plan child, but a JSR223 sampler otherwise. This decision avoids several non-intuitive behaviors of JMeter UDV which are listed in red blocks in the JMeter component documentationopen in new window.

    Internally using a JSR223 sampler, allows DSL users to properly scope a variable to where it is placed (eg: defining a variable in one thread has no effect on other threads or thread groups), set the value when it's actually needed (not just at the beginning of test plan execution), and support cross-variable references (i.e.: if var1=test and var2=${var1}, then the value of var2 would be solved to test).

    When vars() is located as a direct child of the test plan, due to the usage of UDV, declared variables will be available to all thread groups and no variable cross-reference is supported.

    Check DslVariablesopen in new window for more details.

    Properties

    You might reach a point where you want to pass some parameter to the test plan or want to share some object or data that is available for all threads to use. In such scenarios, you can use JMeter properties.

    JMeter properties is a map of keys and values, that is accessible to all threads. To access them you can use ${__P(PROPERTY_NAME)} or equivalent ${__property(PROPERTY_NAME) inside almost any string, props['PROPERTY_NAME'] inside groovy scripts or props.get("PROPERTY_NAME") in lambda expressions.

    To set them, you can use prop() method included in EmbeddedJmeterEngine like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.engines.EmbeddedJmeterEngine;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testProperties() {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("http://myservice.test/${__P(MY_PROP)}")
    +        )
    +    ).runIn(new EmbeddedJmeterEngine()
    +        .prop("MY_PROP", "MY_VAL"));
    +  }
    +
    +}
    +

    Or you can set them in groovy or java code, like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testProperties() {
    +    testPlan(
    +        threadGroup(1, 1,
    +            jsr223Sampler("props.put('MY_PROP', 'MY_VAL')"),
    +            httpSampler("http://myservice.test/${__P(MY_PROP)}")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Or you can even load them from a file, which might be handy to have different files with different values for different execution profiles (eg: different environments). Eg:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.engines.EmbeddedJmeterEngine;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testProperties() {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("http://myservice.test/${__P(MY_PROP)}")
    +        )
    +    ).runIn(new EmbeddedJmeterEngine()
    +        .propertiesFile("my.properties"));
    +  }
    +
    +}
    +

    TIP

    You can put any object (not just strings) in properties, but only strings can be accessed via ${__P(PROPERTY_NAME)} and ${__property(PROPERTY_NAME)}.

    Being able to put any kind of object allows you to do very powerful stuff, like implementing a custom cache, or injecting some custom logic to a test plan.

    TIP

    You can also specify properties through JVM system properties either by setting JVM parameter -D or using System.setProperty() method.

    When properties are set as JVM system properties, they are not accessible via props[PROPERTY_NAME] or props.get("PROPERTY_NAME"). If you need to access them from groovy or java code, then use props.getProperty("PROPERTY_NAME") instead.

    WARNING

    JMeter properties can currently only be used with EmbeddedJmeterEngine, so use them sparingly and prefer other mechanisms when available.

    Test resources

    When working with tests in maven projects, even gradle in some scenarios, it is usually necessary to use files hosted in src/test/resources. For example CSV files for csvDataSet, a file to be used by an httpSampler, some JSON for comparison, etc. The DSL provides testResource as a handy shortcut for such scenarios. Here is a simple example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testProperties() throws IOException {
    +    testPlan(
    +        csvDataSet(testResource("users.csv")), // gets users info from src/test/resources/users.csv
    +        threadGroup(1, 1,
    +            httpSampler("http://myservice.test/users/${USER_ID}")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Check TestResourceopen in new window for some further details.

    Protocols

    HTTP

    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 JmeterDslopen in new window and DslHttpSampleropen in new window to explore all available features.

    Methods & body

    As previously seen, you can do simple gets and posts like in the following snippet:

    httpSampler("http://my.service") // A simple get
    +httpSampler("http://my.service")
    +    .post("{\"field\":\"val\"}", Type.APPLICATION_JSON) // simple post
    +

    But you can also use additional methods to specify any HTTP method and body:

    httpSampler("http://my.service")
    +  .method(HTTPConstants.PUT)
    +  .contentType(Type.APPLICATION_JSON)
    +  .body("{\"field\":\"val\"}")
    +

    Additionally, when in need to generate dynamic URLs or bodies, you can use lambda expressions (as previously seen in some examples):

    httpSampler("http://my.service")
    +  .post(s -> buildRequestBody(s.vars), Type.TEXT_PLAIN)
    +httpSampler("http://my.service")
    +  .body(s -> buildRequestBody(s.vars))
    +httpSampler(s -> buildRequestUrl(s.vars)) // buildRequestUrl is just an example of a custom method you could implement with your own logic
    +

    WARNING

    As previously mentioned, even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    Parameters

    In many cases, you will need to specify some URL query string parameters or URL encoded form bodies. For these cases, you can use param method as in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    String 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(HTTPConstants.POST) // POST 
    +                .param("name", "wooden chair")
    +            )
    +    ).run();
    +  }
    +
    +}
    +

    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 rawParam method instead which does not apply any encoding to the parameter name or value, and send it as is.

    Headers

    You might have already noticed in some of the examples that we have shown already some ways to set some headers. For instance, in the following snippet Content-Type header is being set in two different ways:

    httpSampler("http://my.service")
    +  .post("{\"field\":\"val\"}", Type.APPLICATION_JSON)
    +httpSampler("http://my.service")
    +  .contentType(Type.APPLICATION_JSON)
    +

    These are handy methods to specify the 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")
    +

    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 httpHeaders like this:

    testPlan(
    +    threadGroup(2, 10,
    +        httpHeaders()
    +          .header("X-Header", "val1"),
    +        httpSampler("http://my.service"),
    +        httpSampler("http://my.service/users")
    +    )
    +).run();
    +

    TIP

    You can also use lambda expressions for dynamically building HTTP Headers, but the same limitations apply as in other cases (running in BlazeMeter, OctoPerf, Azure, or using generated JMX file).

    Authentication

    When in need to authenticate user associated to an HTTP request you can either use httpAuth or custom logic (with HTTP headers, regex extractors, variables, and other potential elements) to properly generate the required requests.

    httpAuth greatly simplifies common scenarios like this example using basic auth:

    String baseUrl = "http://my.service";
    +testPlan(
    +    httpAuth()
    +        .basicAuth(baseUrl, System.getenv("AUTH_USER"), System.getenv("AUTH_PASSWORD")),
    +    threadGroup(2, 10,
    +        httpSampler(baseUrl + "/login"),
    +        httpSampler(baseUrl + "/users")
    +    )
    +).run();
    +

    TIP

    Even though you can specify an empty base URL to match any potential request, don't do it. Defining a non-specific enough base URL, may leak credentials to unexpected sites, for example, when used in combination with downloadEmbeddedResources().

    TIP

    Avoid including credentials in repository where code is hosted, which might lead to security leaks.

    In provided example credentials are obtained from environment variable that have to be predefined by user when running tests, but you can also use other approaches to avoid security leaks.

    Also take into consideration that if you use jtlWriter and chose to store HTTP request headers and/or bodies, then JTL could include used credentials and might be also a potential source for security leaks.

    TIP

    Http Authorization Manager, the element used by httpAuth, automatically adds the Authorization header for each request that starts with the given base url. If you need more control (e.g.: only send the header in the first request or under certain condition), you might add httpAuth only to specific requests or just build custom logic through usage of httpHeaders, regexExtractor and jsr223PreProcessor.

    TIP

    Currently httpAuth() only provides basicAuth method. If you need other scenarios, please let us know by creating an issue in the repositoryopen in new window.

    You can check additional details in DslAuthManageropen in new window.

    Multipart requests

    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 bodyPart and bodyFilePart methods like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.apache.http.entity.ContentType;
    +import org.apache.jmeter.protocol.http.util.HTTPConstants;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("https://myservice.com/report")
    +                .method(HTTPConstants.POST)
    +                .bodyPart("myText", "Hello World", ContentType.TEXT_PLAIN)
    +                .bodyFilePart("myFile", "myReport.xml", ContentType.TEXT_XML)
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Cookies & caching

    jmeter-java-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:

    testPlan(
    +    httpCookies().disable(),
    +    httpCache().disable(),
    +    threadGroup(2, 10,
    +        httpSampler("http://my.service")
    +    )
    +)
    +

    Timeouts

    By default, JMeter uses system default configurations for connection and response timeouts (maximum time for a connection to be established or a server response after a request, before it fails). This is might make the test behave different depending on the machine where it runs. To avoid this, it is recommended to always set these values. Here is an example:

    testPlan(
    +    httpDefaults()
    +        .connectionTimeout(Duration.ofSeconds(10))
    +        .responseTimeout(Duration.ofMinutes(1)),
    +    threadGroup(2, 10,
    +        httpSampler("http://my.service")
    +    )
    +)
    +

    WARNING

    Currently we are using same defaults as JMeter to avoid breaking existing test plans executions, but in a future major version we plan to change default setting to avoid the common pitfall previously mentioned.

    Connections

    jmeter-java-dsl, as JMeter (and also K6), by default reuses HTTP connections between thread iterations to avoid common issues with port and file descriptors exhaustion which require manual OS tuning and may manifest in many ways.

    This decision implies that the load generated from 10 threads and 100 iterations is not the same as the one generated by 1000 real users with up to 10 concurrent users in a given time, since the load imposed by each user connection and disconnection would only be generated once for each thread.

    If you need for each iteration to reset connections you can use something like this:

    httpDefaults()
    +    .resetConnectionsBetweenIterations()
    +

    If you use this setting you might want to take a look at "Config your environment" section of this articleopen in new window to avoid port and file descriptors exhaustion.

    TIP

    Connections are configured by default with a TTL (time-to-live) of 1 minute, which you can easily change like this:

    httpDefaults()
    +    .connectionTtl(Duration.ofMinutes(10))
    +
    • This and resetConnectionsBetweenIterations apply at the JVM level (due to JMeter limitation), so they affect all requests in the test plan and other ones potentially running in the same JVM instance.

    WARNING

    Using clientImpl(HttpClientImpl.JAVA) will ignore any of the previous settings and will reuse connections depending on JVM implementation.

    Embedded resources

    Sometimes you may need to reproduce a browser behavior, downloading for a given URL all associated resources (images, frames, etc.).

    jmeter-java-dsl allows you to easily reproduce this scenario by using the downloadEmbeddedResources method in httpSampler like in the following example:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(5, 10,
    +            httpSampler("http://my.service/")
    +                .downloadEmbeddedResources()
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    This will make JMeter automatically parse the HTTP response for embedded resources, download them and register embedded resources downloads as sub-samples of the main sample.

    Check JMeter documentationopen in new window for additional details on downloaded embedded resources.

    TIP

    You can use downloadEmbeddedResourcesNotMatching(urlRegex) and downloadEmbeddedResourcesMatching(urlRegex) methods if you need to ignore, or only download, some embedded resources requests. For example, when some requests are not related to the system under test.

    WARNING

    The DSL, unlike JMeter, uses by default concurrent download of embedded resources (with up to 6 parallel downloads), which is the most used scenario to emulate browser behavior.

    WARNING

    Using downloadEmbeddedResources doesn't allow to download all resources that a browser could download, since it does not execute any JavaScript. For instance, resources URLs solved through JavaScript or direct JavaScript requests will not be requested. Even with this limitation, in many cases just downloading "static" resources is a good enough solution for performance testing.

    Redirects

    When jmeter-java-dsl (using JMeter logic) detects a redirection, it will automatically do a request to the redirected URL and register the redirection as a sub-sample of the main request.

    If you want to disable such logic, you can just call .followRedirects(false) in a given httpSampler.

    HTTP defaults

    Whenever you need to use some repetitive value or common setting among HTTP samplers (and any part of the test plan) the preferred way (due to readability, debugability, traceability, and in some cases simplicity) is to create a Java variable or custom builder method.

    For example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.http.DslHttpSampler;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void performanceTest() throws IOException {
    +    String host = "myservice.my";
    +    testPlan(
    +        threadGroup(10, 100,
    +            productCreatorSampler(host, "Rubber"),
    +            productCreatorSampler(host, "Pencil")
    +        )
    +    ).run();
    +  }
    +
    +  private DslHttpSampler productCreatorSampler(String host, String productName) {
    +    return httpSampler("https://" + host + "/api/product")
    +        .post("{\"name\": \"" + productName + "\"}", ContentType.APPLICATION_JSON);
    +  }
    +
    +}
    +

    In some cases though, it might be simpler to just use provided httpDefaults method, like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void performanceTest() throws IOException {
    +    testPlan(
    +        httpDefaults()
    +            .url("https://myservice.my")
    +            .downloadEmbeddedResources(),
    +        threadGroup(10, 100,
    +            httpSampler("/products"),
    +            httpSampler("/cart")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Check DslHttpDefaultsopen in new window for additional details on available default options.

    Overriding URL protocol, host or port

    In some cases, you might want to use a default base URL but some particular requests may require some part of the URL to be different (eg: protocol, host, or port).

    The preferred way (due to maintainability, language & IDE provided features, traceability, etc) of doing this, as with defaults, is using java code. Eg:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    String protocol = "https://";
    +    String host = "myservice.com";
    +    String baseUrl = protocol + host;
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler(baseUrl + "/products"),
    +            httpSampler(protocol + "api." + host + "/cart"),
    +            httpSampler(baseUrl + "/stores")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    But in some cases, this might be too verbose, or unnatural for users with existing JMeter knowledge. In such cases you can use provided methods (protocol, host & port) to just specify the part you want to modify for the sampler like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpDefaults()
    +                .url("https://myservice.com"),
    +            httpSampler("/products"),
    +            httpSampler("/cart")
    +                .host("subDomain.myservice.com"),
    +            httpSampler("/stores")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    Proxy

    Sometimes, due to company policies, some infrastructure requirement or just to further analyze or customize requests, for example, through the usage of tools like fiddleropen in new window and mitmproxyopen in new window, you need to specify a proxy server through which HTTP requests are sent to their final destination. This can be easily done with proxy method, like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            httpSampler("https://myservice.com")
    +                .proxy("http://myproxy:8081")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    TIP

    You can also specify proxy authentication parameters with proxy(url, username, password) method.

    TIP

    When you need to set a proxy for several samplers, use httpDefaults().proxy methods.

    GraphQL

    When you want to test a GraphQL service, having properly set each field in an HTTP request and knowing the exact syntax for each of them, can quickly start becoming tedious. For this purpose, jmeter-java-dsl provides graphqlSampler. To use it you need to include this dependency:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-graphql</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-graphql:1.29'
    +

    And then you can make simple GraphQL requests like this:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.graphql.DslGraphqlSampler.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    String url = "https://myservice.com";
    +    testPlan(
    +        threadGroup(1, 1,
    +            graphqlSampler(url, "{user(id: 1) {name}}"),
    +            graphqlSampler(url, "query UserQuery($id: Int) { user(id: $id) {name}}")
    +                .operationName("UserQuery")
    +                .variable("id", 2)
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    TIP

    GraphQL Sampler is based on HTTP Sampler, so all test elements that affect HTTP Samplers, like httpHeaders, httpCookies, httpDefaults, and JMeter properties, also affect GraphQL sampler.

    WARNING

    grapqlSampler sets by default application/json Content-Type header.

    This has been done to ease the most common use cases and to avoid users the common pitfall of missing the proper Content-Type header value.

    If you need to modify graphqlSampler content type to be other than application/json, then you can use contentType method, potentially parameterizing it to reuse the same value in multiple samplers like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.graphql.DslGraphqlSampler.*;
    +
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.graphql.DslGraphqlSampler;
    +
    +public class PerformanceTest {
    +
    +  private DslGraphqlSampler myGraphqlRequest(String query) {
    +    return graphqlSampler("https://myservice.com", query)
    +        .contentType(ContentType.create("myContentType"));
    +  }
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            myGraphqlRequest("{user(id: 1) {name}}"),
    +            myGraphqlRequest("{user(id: 5) {address}}")
    +        )
    +    ).run();
    +  }
    +
    +}
    +

    JDBC and databases interactions

    Several times you will need to interact with a database to either set it to a known state while setting up the test plan, clean it up while tearing down the test plan, or even check or generate some values in the database while the test plan is running.

    For these use cases, you can use JDBC DSL-provided elements.

    Including the following dependency in your project:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-jdbc</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-jdbc:1.29'
    +

    And adding a proper JDBC driver for your database, like this example for PostgreSQL:

    <dependency>
    +  <groupId>org.postgresql</groupId>
    +  <artifactId>postgresql</artifactId>
    +  <version>42.3.1</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'org.postgresql:postgresql:42.3.1'
    +

    You can interact with the database like this:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.jdbc.JdbcJmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.sql.Types;
    +import java.time.Duration;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import org.postgresql.Driver;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +import us.abstracta.jmeter.javadsl.jdbc.DslJdbcSampler;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    String jdbcPoolName = "pgLocalPool";
    +    String productName = "dsltest-prod";
    +    DslJdbcSampler cleanUpSampler = jdbcSampler(jdbcPoolName,
    +        "DELETE FROM products WHERE name = '" + productName + "'")
    +        .timeout(Duration.ofSeconds(10));
    +    TestPlanStats stats = testPlan(
    +        jdbcConnectionPool(jdbcPoolName, Driver.class, "jdbc:postgresql://localhost/my_db")
    +            .user("user")
    +            .password("pass"),
    +        setupThreadGroup(
    +            cleanUpSampler
    +        ),
    +        threadGroup(5, 10,
    +            httpSampler("CreateProduct", "http://my.service/products")
    +                .post("{\"name\", \"" + productName + "\"}", ContentType.APPLICATION_JSON),
    +            jdbcSampler("GetProductsIdsByName", jdbcPoolName,
    +                "SELECT id FROM products WHERE name=?")
    +                .param(productName, Types.VARCHAR)
    +                .vars("PRODUCT_ID")
    +                .timeout(Duration.ofSeconds(10)),
    +            httpSampler("GetLatestProduct",
    +                "http://my.service/products/${__V(PRODUCT_ID_${PRODUCT_ID_#})}")
    +        ),
    +        teardownThreadGroup(
    +            cleanUpSampler
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    TIP

    Always specify a query timeout to quickly identify unexpected behaviors in queries.

    TIP

    Don't forget proper WHERE conditions in UPDATES and DELETES, and proper indexes for table columns participating in WHERE conditions 😊.

    Check JdbcJmeterDslopen in new window for additional details and options and JdbcJmeterDslTestopen in new window for additional examples.

    Java API performance testing

    Sometimes JMeter provided samplers are not enough for testing a particular technology, custom code, or service that requires some custom code to interact with. For these cases, you might use jsr223Sampler which allows you to use custom logic to generate a sample result.

    Here is an example for load testing a Redis server:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class TestRedis {
    +
    +  @Test
    +  public void shouldGetExpectedSampleResultWhenJsr223SamplerWithLambdaAndCustomResponse()
    +      throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            jsr223Sampler("import redis.clients.jedis.Jedis\n"
    +                + "Jedis jedis = new Jedis('localhost', 6379)\n"
    +                + "jedis.connect()\n"
    +                + "SampleResult.connectEnd()\n"
    +                + "jedis.set('foo', 'bar')\n"
    +                + "return jedis.get(\"foo\")")
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofMillis(500));
    +  }
    +
    +}
    +

    TIP

    Remember to add any particular dependencies required by your code. For example, the above example requires this dependency:

    <dependency>
    +  <groupId>redis.clients</groupId>
    +  <artifactId>jedis</artifactId>
    +  <version>3.6.0</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'redis.clients:jedis:3.6.0'
    +

    You can also use Java lambdas instead of Groovy script to take advantage of IDEs auto-completion, Java type safety, and less CPU consumption:

    jsr223Sampler(v -> {
    +    SampleResult result = v.sampleResult;
    +    Jedis jedis = new Jedis("localhost", 6379);
    +    jedis.connect();
    +    result.connectEnd();
    +    jedis.set("foo", "bar");
    +    result.setResponseData(jedis.get("foo"), StandardCharsets.UTF_8.name());
    +})
    +

    WARNING

    As previously mentioned, even though using Java Lambdas has several benefits, they are also less portable. Check this section for more details.

    You may even use some custom logic that executes a particular logic when a thread group thread is created and finished. Here is an example:

    public class TestRedis {
    +
    +  public static class RedisSampler implements SamplerScript, ThreadListener {
    +
    +    private Jedis jedis;
    +
    +    @Override
    +    public void threadStarted() {
    +      jedis = new Jedis("localhost", 6379);
    +      jedis.connect();
    +    }
    +
    +    @Override
    +    public void runScript(SamplerVars v) {
    +      jedis.set("foo", "bar");
    +      v.sampleResult.setResponseData(jedis.get("foo"), StandardCharsets.UTF_8.name());
    +    }
    +
    +    @Override
    +    public void threadFinished() {
    +      jedis.close();
    +    }
    +
    +  }
    +
    +  @Test
    +  public void shouldGetExpectedSampleResultWhenJsr223SamplerWithLambdaAndCustomResponse()
    +      throws IOException {
    +    TestPlanStats stats = testPlan(
    +        threadGroup(2, 10,
    +            jsr223Sampler(RedisSampler.class)
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofMillis(500));
    +  }
    +
    +}
    +

    TIP

    You can also make your class implement TestIterationListener to execute custom logic on each thread group iteration start, or LoopIterationListener to execute some custom logic on each iteration start (for example, each iteration of a forLoop).

    TIP

    When using public static classes in jsr223Sampler take into consideration that one instance of the class is created for each thread group thread and jsr223Sampler instance.

    Note: jsr223Sampler is very powerful, but also makes code and test plans harder to maintain (as with any custom code) compared to using JMeter built-in samplers. So, in general, prefer using JMeter-provided samplers if they are enough for the task at hand, and use jsr223Sampler sparingly.

    Check DslJsr223Sampleropen in new window for more details and additional options.

    Selenium

    With JMeter DSL is quite simple to integrate your existing selenium scripts into performance tests. One common use case is to do real user monitoring or synthetics monitoring (get time spent in particular parts of a Selenium script) while the backend load is being generated.

    Here is an example of how you can do this with JMeter DSL:

    public class PerformanceTest {
    +
    +  public static class SeleniumSampler implements SamplerScript, ThreadListener {
    +
    +    private WebDriver driver;
    +
    +    @Override
    +    public void threadStarted() {
    +      driver = new ChromeDriver(); // you can invoke existing set up logic to reuse it
    +    }
    +
    +    @Override
    +    public void runScript(SamplerVars v) {
    +      driver.get("https://mysite"); // you can invoke existing selenium script for reuse here
    +    }
    +
    +    @Override
    +    public void threadFinished() {
    +      driver.close(); // you can invoke existing tear down logic to reuse it
    +    }
    +
    +  }
    +
    +  @Test
    +  public void shouldGetExpectedSampleResultWhenJsr223SamplerWithLambdaAndCustomResponse()
    +      throws IOException {
    +    Duration testPlanDuration = Duration.ofMinutes(10);
    +    TestPlanStats stats = testPlan(
    +        threadGroup(1, testPlanDuration,
    +            jsr223Sampler("Real User Monitor", SeleniumSampler.class)
    +        ),
    +        threadGroup(100, testPlanDuration,
    +            httpSampler("https://mysite/products")
    +                .post("{\"name\": \"test\"}", Type.APPLICATION_JSON)
    +        )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofMillis(500));
    +  }
    +
    +}
    +

    Check previous section for more details on jsr223Sampler.

    Custom or yet not supported test elements

    Whenever you find some JMeter test element or feature that is not yet supported by the DSL, we strongly encourage you to request it as an issue hereopen in new window or even contribute it to the DSL (check Contributing guideopen in new window) so the entire community can benefit from it.

    In some cases though, you might have some private custom test element that you don't want to publish or share with the rest of the community, or you are just really in a hurry and want to use it while the proper support is included in the DSL.

    For such cases, the preferred approach is implementing a builder class for the test element. Eg:

    import org.apache.jmeter.testelement.TestElement;
    +import us.abstracta.jmeter.javadsl.core.samplers.BaseSampler;
    +
    +public class DslCustomSampler extends BaseSampler<DslCustomSampler> {
    +
    +  private String myProp;
    +
    +  private DslCustomSampler(String name) {
    +    super(name, CustomSamplerGui.class); // you can pass null here if custom sampler is a test bean
    +  }
    +
    +  public DslCustomSampler myProp(String val) {
    +    this.myProp = val;
    +    return this;
    +  }
    +
    +  @Override
    +  protected TestElement buildTestElement() {
    +    CustomSampler ret = new CustomSampler();
    +    ret.setMyProp(myProp);
    +    return ret;
    +  }
    +
    +  public static DslCustomSampler customSampler(String name) {
    +    return new DslCustomSampler(name);
    +  }
    +
    +}
    +

    Which you can use as any other JMeter DSL component, like in this example:

    import static us.abstracta.jmeter.javadsl.DslCustomSampler.*;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            customSampler("mySampler")
    +                .myProp("myVal")
    +            )
    +    ).run();
    +  }
    +
    +}
    +

    This approach allows for easy reuse, compact and simple usage in tests, and you might even create your own CustomJmeterDsl class containing builder methods for many custom components.

    Alternatively, when you want to skip creating subclasses, you might use the DSL wrapper module.

    Include the module on your project:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl-wrapper</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +
    testImplementation 'us.abstracta.jmeter:jmeter-java-dsl-wrapper:1.29'
    +

    And use a wrapper like in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +import static us.abstracta.jmeter.javadsl.wrapper.WrapperJmeterDsl.*;
    +
    +import org.junit.jupiter.api.Test;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void test() throws Exception {
    +    testPlan(
    +        threadGroup(1, 1,
    +            testElement("mySampler", new CustomSamplerGui()) // for test beans you can just provide the test bean instance
    +                .prop("myProp","myVal")
    +            )
    +    ).run();
    +  }
    +
    +}
    +

    Check WrapperJmeterDslopen in new window for more details and additional wrappers.

    JMX support

    Save as JMX

    In case you want to load a test plan in JMeter GUI, you can save it just invoking saveAsJMX method in the test plan as in the following example:

    import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +public class SaveTestPlanAsJMX {
    +
    +  public static void main(String[] args) throws Exception {
    +    testPlan(
    +        threadGroup(2, 10,
    +            httpSampler("http://my.service")
    +        )
    +    ).saveAsJmx("dsl-test-plan.jmx");
    +  }
    +
    +}
    +

    This can be helpful to share a Java DSL defined test plan with people not used to the DSL or to use some JMeter feature (or plugin) that is not yet supported by the DSL (but, we strongly encourage you to report it as an issue hereopen in new window so we can include such support into the DSL for the rest of the community).

    TIP

    If you get any error (like CannotResolveClassException) while loading the JMX in JMeter GUI, you can try copying jmeter-java-dsl jar (and any other potential modules you use) to JMeter lib directory, restart JMeter and try loading the JMX again.

    TIP

    If you want to migrate changes done in JMX to the Java DSL, you can use jmx2dsl as an accelerator. The resulting plan might differ from the original one, so sometimes it makes sense to use it, and some it is faster just to port the changes manually.

    WARNING

    If you use JSR223 Pre- or Post-processors with Java code (lambdas) instead of strings or use one of the HTTP Sampler methods which receive a function as a parameter, then the exported JMX will not work in JMeter GUI. You can migrate them to use jsrPreProcessor with string scripts instead.

    Run JMX file

    jmeter-java-dsl also provides means to easily run a test plan from a JMX file either locally, in BlazeMeter (through previously mentioned jmeter-java-dsl-blazemeter module), OctoPerf (through jmeter-java-dsl-octoperf module), or Azure Load testing (through jmeter-java-dsl-azure module). Here is an example:

    import static org.assertj.core.api.Assertions.assertThat;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.DslTestPlan;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class RunJmxTestPlan {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = DslTestPlan.fromJmx("test-plan.jmx").run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    This can be used to just run existing JMX files, or when DSL has no support for some JMeter functionality or plugin (although you can use wrappers for this), and you need to use JMeter GUI to build the test plan but still want to use jmeter-java-dsl to run the test plan embedded in Java test or code.

    TIP

    When the JMX uses some custom plugin or JMeter protocol support, you might need to add required dependencies to be able to run the test in an embedded engine. For example, when running a TN3270 JMX test plan using RTE plugin you will need to add the following repository and dependencies:

    <repositories>
    +  <repository>
    +    <id>jitpack.io</id>
    +    <url>https://jitpack.io</url>
    +  </repository>
    +</repositories>
    +
    +<dependencies>
    +  ...
    +  <dependency>
    +    <groupId>com.github.Blazemeter</groupId>
    +    <artifactId>RTEPlugin</artifactId>
    +    <version>3.1</version>
    +    <scope>test</scope>
    +  </dependency>
    +  <dependency>
    +    <groupId>com.github.Blazemeter</groupId>
    +    <artifactId>dm3270</artifactId>
    +    <version>0.12.3-lib</version>
    +    <scope>test</scope>
    +  </dependency>
    +</dependencies>
    +
    + + + diff --git a/index.html b/index.html new file mode 100644 index 00000000..138fd1e0 --- /dev/null +++ b/index.html @@ -0,0 +1,65 @@ + + + + + + + + + jmeter-java-dsl + + + + +

    Simple JMeteropen in new window performance tests Java API

    User Guide →

    💙 Git, IDE & Programmers Friendly

    Simple way of defining performance tests that takes advantage of IDEs autocompletion and inline documentation.

    💪 JMeter ecosystem & community

    Use the most popular performance tool and take advantage of the wide support of protocols and tools.

    😎 Built-in features & extensibility

    Built-in additional features which ease usage (like jmx2dsl and recorder) and CI/CD pipelines integration.

    Example

    Add dependency to your project:

    <dependency>
    +  <groupId>us.abstracta.jmeter</groupId>
    +  <artifactId>jmeter-java-dsl</artifactId>
    +  <version>1.29</version>
    +  <scope>test</scope>
    +</dependency>
    +

    Create performance test:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +            threadGroup(2, 10,
    +                    httpSampler("http://my.service")
    +            )
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +
    +}
    +

    You can use this projectopen in new window as a starting point.

    Hear It From Our Community

    + + + diff --git a/logo.svg b/logo.svg new file mode 100644 index 00000000..9969b4df --- /dev/null +++ b/logo.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/motivation/index.html b/motivation/index.html new file mode 100644 index 00000000..7f55f539 --- /dev/null +++ b/motivation/index.html @@ -0,0 +1,64 @@ + + + + + + + + + Motivation | jmeter-java-dsl + + + + +

    Motivation

    There are many tools to script performance/load tests, being JMeteropen in new window and Gatlingopen in new window the most popular ones.

    Here we explore some alternatives, their pros & cons, and the main motivations behind the development of jmeter-java-dsl.

    Alternatives analysis

    JMeter

    JMeter is great for people with no programming knowledge since it provides a graphical interface to create test plans and run them. Additionally, it is the most popular tool (with a lot of supporting tools built on it) and has a big amount of supported protocols and plugins making it very versatile.

    But, JMeter has some downsides as well: sometimes it might be slow to create test plans in JMeter GUI and you can't get the full picture of the test plan unless you dig in every tree node to check its properties. Furthermore, it doesn't provide a simple programmer-friendly API (you can check hereopen in new window for an example of how to run JMeter programmatically without jmeter-java-dsl), nor a Git-friendly format (too verbose and hard to review). For example, for this test plan:

    import static org.assertj.core.api.Assertions.assertThat;
    +import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
    +
    +import java.io.IOException;
    +import java.time.Duration;
    +import java.time.Instant;
    +import org.apache.http.entity.ContentType;
    +import org.junit.jupiter.api.Test;
    +import us.abstracta.jmeter.javadsl.core.TestPlanStats;
    +
    +public class PerformanceTest {
    +
    +  @Test
    +  public void testPerformance() throws IOException {
    +    TestPlanStats stats = testPlan(
    +      threadGroup(2, 10,
    +        httpSampler("http://my.service")
    +          .post("{\"name\": \"test\"}", ContentType.APPLICATION_JSON)
    +      ),
    +      //this is just to log details of each request stats
    +      jtlWriter("target/jtls")
    +    ).run();
    +    assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
    +  }
    +  
    +}
    +

    In JMeter, you would need a JMX file like thisopen in new window, and even then, it wouldn't be as simple to do assertions on collected statistics as in provided example.

    Gatling

    Gatling does provide a simple API and Git-friendly format but requires scala knowledge and environment [1]. Additionally, it doesn't provide as a rich environment as JMeter (protocol support, plugins, tools) and requires learning a new framework for testing (if you already use JMeter, which is the most popular tool).

    Taurus

    Taurusopen in new window is another open-source tool that allows specifying tests in a Git-friendly yaml syntax, and provides additional features like pass/fail criteria and easier CI/CD integration. But, this tool requires a python environment, in addition to the java environment. Additionally, there is no built-in GUI or IDE auto-completion support, which makes it harder to discover and learn the actual syntax. Finally, Taurus syntax only supports a subset of the features JMeter provides.

    ruby-dsl

    Finally, ruby-dslopen in new window is also an open-source library that allows specifying and running in ruby custom DSL JMeter test plans. This is the most similar tool to jmeter-java-dsl, but it requires ruby (in addition to the java environment) with the additional performance impact, does not follow the same naming and structure convention as JMeter, and lacks debugging integration with JMeter execution engine.

    jmeter-java-dsl

    jmeter-java-dsl tries to get the best of these tools by providing a simple java API with Git friendly format to run JMeter tests, taking advantage of all JMeter benefits and knowledge and also providing many of the benefits of Gatling scripting. As shown in the previous example, it can be easily executed with JUnit, modularized in code, and easily integrated into any CI/CD pipeline. Additionally, it makes it easy to debug the execution of test plans with the usual IDE debugger tools. Finally, as with most Java libraries, you can use it not only in a Java project but also in projects of most JVM languages (like kotlin, scala, groovy, etc.).

    Comparison Table

    Here is a table with a summary of the main pros and cons of each tool:

    ToolProsCons
    JMeter👍 GUI for non programmers
    👍 Popularity
    👍 Protocols Support
    👍 Documentation
    👍 Rich ecosystem
    👎 Slow test plan creation
    👎 No VCS friendly format
    👎 Not programmers friendly
    👎 No simple CI/CD integration
    Gatling👍 VCS friendly
    👍 IDE friendly (auto-complete and debug)
    👍 Natural CI/CD integration
    👍 Natural code modularization and reuse
    👍 Less resources (CPU & RAM) usage
    👍 All details of simple test plans at a glance
    👍 Simple way to do assertions on statistics
    👎 Scala knowledge and environment required [1]
    👎 Smaller set of protocols supported
    👎 Less documentation & tooling
    👎 Live statistics charts & grafana integration only available in enterprise version
    Taurus👍 VCS friendly
    👍 Simple CI/CD integration
    👍 Unified framework for running any type of test
    👍 built-in support for running tests at scale
    👍 All details of simple test plans at a glance
    👍 Simple way to do assertions on statistics
    👎 Both Java and Python environments required
    👎 Not as simple to discover (IDE auto-complete or GUI) supported functionality
    👎 Not complete support of JMeter capabilities (nor in the roadmap)
    ruby-dsl👍 VCS friendly
    👍 Simple CI/CD integration
    👍 Unified framework for running any type of test
    👍 built-in support for running tests at scale
    👍 All details of simple test plans at a glance
    👎 Both Java and Ruby environments required
    👎 Not following same naming convention and structure as JMeter
    👎 Not complete support of JMeter capabilities (nor in the roadmap)
    👎 No integration for debugging JMeter code
    jmeter-java-dsl👍 VCS friendly
    👍 IDE friendly (auto-complete and debug)
    👍 Natural CI/CD integration
    👍 Natural code modularization and reuse
    👍 Existing JMeter documentation
    👍 Easy to add support for JMeter supported protocols and new plugins
    👍 Could easily interact with JMX files and take advantage of JMeter ecosystem
    👍 All details of simple test plans at a glance
    👍 Simple way to do assertions on statistics
    👎 Basic Java knowledge required
    👎 Same resources (CPU & RAM) usage as JMeter

    Notes

    1. One year after jmeter-java-dsl release, on November 2021, Gatling released 3.7 versionopen in new window, including a Java friendly API for existing Gatling Scala API. This greatly simplifies usage for Java users and is a great addition to Gatling.

      As a side note, take into consideration that the underlying code is still Scala and async model-based, which makes debugging and understanding it harder for Java developers than JMeter code. Additionally, the model is still tied to Simulator classes and maven (gradle or sbt) plugin to be able to run the tests, compared to the simplicity and flexibility of jmeter-java-dsl tests execution.

    + + + diff --git a/support/index.html b/support/index.html new file mode 100644 index 00000000..b0115521 --- /dev/null +++ b/support/index.html @@ -0,0 +1,38 @@ + + + + + + + + + Support | jmeter-java-dsl + + + + +

    Support

    Community Support

    The JMeter DSL project has a vibrant and active community that provides extensive support, on a best effort basis, to its users. Community support is primarily offered through the following channels:

    The community is actively involved in proposing new improvements, answering questions, assisting in design decisions, and submitting pull requests. Together, we strive to enhance the capabilities and usability of JMeter DSL.

    Enterprise Support by Abstracta

    In addition to community support, Abstractaopen in new window offers enterprise-level support for JMeter DSL users. Abstracta is the main supporter of JMeter DSL development and provides specialized professional services to ensure the success of organizations using JMeter DSL. With Abstracta's enterprise support, you can accelerate your JMeter DSL implementation and have access to:

    • Dedicated support team : Get prompt answers and peace of mind from a dedicated support team with the expertise to help you resolve issues faster.
    • Customizations: Receive tailored solutions to meet your specific requirements.
    • Consulting services: Access a team of experts to fine-tune your JMeter DSL usage, speed up implementation, work on your performance testing strategy and overall testing processes.

    Abstracta is committed to helping organizations succeed with JMeter DSL by providing comprehensive support and specialized services tailored to your enterprise needs.

    To explore Abstracta's enterprise support options or discuss your specific needs, please contact the Abstracta teamopen in new window.

    Industry Support

    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:

    + + + diff --git a/testimonials.html b/testimonials.html new file mode 100644 index 00000000..07789028 --- /dev/null +++ b/testimonials.html @@ -0,0 +1,38 @@ + + + + + + + + + jmeter-java-dsl + + + + + + + +