Skip to content

Commit

Permalink
Add support for regex extractors
Browse files Browse the repository at this point in the history
  • Loading branch information
rabelenda committed Aug 31, 2023
1 parent 1ec8c77 commit 8cef5cc
Show file tree
Hide file tree
Showing 19 changed files with 418 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using WireMock.FluentAssertions;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;

namespace Abstracta.JmeterDsl.Core.PostProcessors
{
using static JmeterDsl;

public class DslRegexExtractorTest
{
private WireMockServer _wiremock;

[SetUp]
public void SetUp()
{
_wiremock = WireMockServer.Start();
_wiremock.Given(Request.Create().WithPath("/"))
.RespondWith(Response.Create()
.WithStatusCode(200));
}

[TearDown]
public void TearDown() =>
_wiremock.Stop();

[Test]
public void ShouldExtractVariableWhenSimpleRegexExtractorMatches()
{
var user = "test";
var userParam = "user=";
var userVar = "USER";
TestPlan(
ThreadGroup(1, 1,
DummySampler(userParam + user)
.Children(
RegexExtractor(userVar, userParam + "(.*)")
),
HttpSampler(_wiremock.Url + "/?" + userParam + "${" + userVar + "}")
)).Run();
_wiremock.Should().HaveReceivedACall().AtUrl(_wiremock.Url + "/?" + userParam + user);
}

[Test]
public void ShouldExtractVariableWhenComplexRegexExtractorMatches()
{
var user = "test";
var userParam = "user=";
var userVar = "USER";
TestPlan(
ThreadGroup(1, 1,
DummySampler("OK")
.Url("http://localhost/?" + userParam + "user2&" + userParam + user)
.Children(
RegexExtractor(userVar, "([^&?]+)=([^&]+)")
.MatchNumber(2)
.Template("$2$")
.FieldToCheck(DslRegexExtractor.DslTargetField.RequestUrl)
.Scope(TestElements.DslScopedTestElement<DslRegexExtractor>.DslScope.AllSamples)
),
HttpSampler(_wiremock.Url + "/?" + userParam + "${" + userVar + "}")
)).Run();
_wiremock.Should().HaveReceivedACall().AtUrl(_wiremock.Url + "/?" + userParam + user);
}
}
}
1 change: 1 addition & 0 deletions Abstracta.JmeterDsl/Core/Bridge/BridgeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ private void SerializeObjectToWriter(object val, TextWriter writer)
var testElementConverter = new BridgedObjectConverter();
var builder = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeConverter(new EnumConverter())
.WithTypeConverter(testElementConverter)
.WithTypeConverter(new TimespanConverter());
testElementConverter.ValueSerializer = builder.BuildValueSerializer();
Expand Down
23 changes: 23 additions & 0 deletions Abstracta.JmeterDsl/Core/Bridge/EnumConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Text.RegularExpressions;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;

namespace Abstracta.JmeterDsl.Core.Bridge
{
public class EnumConverter : IYamlTypeConverter
{
public bool Accepts(Type type)
=> type.IsEnum;

public object ReadYaml(IParser parser, Type type)
=> throw new NotImplementedException();

public void WriteYaml(IEmitter emitter, object value, Type type)
=> emitter.Emit(new Scalar(ToSnakeCase(value.ToString())));

private string ToSnakeCase(string val)
=> Regex.Replace(val, @"([a-z0-9])([A-Z])", "$1_$2").ToUpper();
}
}
2 changes: 1 addition & 1 deletion Abstracta.JmeterDsl/Core/DslTestPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Abstracta.JmeterDsl.Core
/// <summary>
/// Represents a JMeter test plan, with associated thread groups and other children elements.
/// </summary>
public class DslTestPlan : TestElementContainer<ITestPlanChild>
public class DslTestPlan : TestElementContainer<DslTestPlan, ITestPlanChild>
{
public DslTestPlan(ITestPlanChild[] children)
: base(null, children)
Expand Down
146 changes: 146 additions & 0 deletions Abstracta.JmeterDsl/Core/PostProcessors/DslRegexExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
namespace Abstracta.JmeterDsl.Core.PostProcessors
{
/// <summary>
/// Allows extracting part of a request or response using regular expressions to store into a
/// variable.
/// <br/>
/// By default, the regular extractor is configured to extract from the main sample (does not include
/// sub samples) response body the first capturing group (part of regular expression that is inside
/// of parenthesis) of the first match of the regex. If no match is found, then the variable will not
/// be created or modified.
/// </summary>
public class DslRegexExtractor : DslVariableExtractor<DslRegexExtractor>
{
private readonly string _regex;
private string _template;
private DslTargetField? _fieldToCheck;

public DslRegexExtractor(string varName, string regex)
: base(null, varName)
{
_regex = regex;
}

public enum DslTargetField
{
/// <summary>
/// Applies the regular extractor to the plain string of the response body.
/// </summary>
ResponseBody,

/// <summary>
/// Applies the regular extractor to the response body replacing all HTML escape codes.
/// </summary>
ResponseBodyUnescaped,

/// <summary>
/// Applies the regular extractor to the string representation obtained from parsing the response
/// body with <a href="http://tika.apache.org/1.2/formats.html">Apache Tika</a>.
/// </summary>
ResponseBodyAsDocument,

/// <summary>
/// Applies the regular extractor to response headers. Response headers is a string with headers
/// separated by new lines and names and values separated by colons.
/// </summary>
ResponseHeaders,

/// <summary>
/// Applies the regular extractor to request headers. Request headers is a string with headers
/// separated by new lines and names and values separated by colons.
/// </summary>
RequestHeaders,

/// <summary>
/// Applies the regular extractor to the request URL.
/// </summary>
RequestUrl,

/// <summary>
/// Applies the regular extractor to response code.
/// </summary>
ResponseCode,

/// <summary>
/// Applies the regular extractor to response message.
/// </summary>
ResponseMessage,
}

/// <summary>
/// Sets the match number to be extracted.
/// <br/>
/// For example, if a response looks like this:
/// <c>user=test&amp;user=tester</c>
/// and you use <c>user=([^&amp;]+)</c> as regular expression, first match (1) would extract
/// <c>test</c> and second match (2) would extract <c>tester</c>.
/// <br/>
/// When not specified, the first match will be used. When 0 is specified, a random match will be
/// used. When negative, all the matches are extracted to variables with name
/// <c>&lt;variableName&gt;_&lt;matchNumber&gt;</c>, the number of matches is stored in
/// <c>&lt;variableName&gt;_matchNr</c>, and default value is assigned to <c>&lt;variableName&gt;</c>.
/// </summary>
/// <param name="matchNumber">specifies the match number to use.</param>
/// <returns>the extractor for further configuration or usage.</returns>
public DslRegexExtractor MatchNumber(int matchNumber)
{
_matchNumber = matchNumber;
return this;
}

/// <summary>
/// Specifies the final string to store in the JMeter Variable.
/// <br/>
/// The string may contain capturing groups (regular expression segments between parenthesis)
/// references by using <c>$&lt;groupId&gt;$</c> expressions (eg: <c>$1$</c> for first group). Check
/// <a href="https://jmeter.apache.org/usermanual/component_reference.html#Regular_Expression_Extractor">JMeter
/// Regular Expression Extractor documentation</a> for more details.
/// <br/>
/// For example, if a response looks like this:
/// <c>[email protected]</c>
/// And you use <c>user=([^&amp;]+)</c> as regular expression. Then <c>$1$-$2$</c> will result in
/// storing in the specified JMeter variable the value <c>tester-abstracta</c>.
/// <br/>
/// When not specified <c>$1$</c> will be used.
/// </summary>
/// <param name="template">specifies template to use for storing in the JMeter variable.</param>
/// <returns>the extractor for further configuration or usage.</returns>
public DslRegexExtractor Template(string template)
{
_template = template;
return this;
}

/// <summary>
/// Sets the default value to be stored in the JMeter variable when the regex does not match.
/// <br/>
/// When match number is negative then the value is always assigned to the variable name.
/// </summary>
/// A common pattern is to specify this value to a known value (e.g.:
/// &lt;VAR&gt;_EXTRACTION_FAILURE) and then add some assertion on the variable to mark request as
/// failure when the match doesn't work.
/// <br/>
/// When not specified then the variable will not be set if no match is found.
/// <param name="defaultValue">specifies the default value to be used.</param>
/// <returns>the extractor for further configuration or usage.</returns>
public DslRegexExtractor DefaultValue(string defaultValue)
{
_defaultValue = defaultValue;
return this;
}

/// <summary>
/// Allows specifying what part of request or response to apply the regular extractor to.
/// <br/>
/// When not specified then the regular extractor will be applied to the response body.
/// </summary>
/// <param name="fieldToCheck">field to apply the regular extractor to.</param>
/// <returns>the extractor for further configuration or usage.</returns>
/// <seealso cref="DslTargetField"/>
public DslRegexExtractor FieldToCheck(DslTargetField fieldToCheck)
{
_fieldToCheck = fieldToCheck;
return this;
}
}
}
21 changes: 21 additions & 0 deletions Abstracta.JmeterDsl/Core/PostProcessors/DslVariableExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Abstracta.JmeterDsl.Core.TestElements;

namespace Abstracta.JmeterDsl.Core.PostProcessors
{
/// <summary>
/// Contains common logic for post processors which extract some value into a variable.
/// </summary>
public abstract class DslVariableExtractor<T> : DslScopedTestElement<T>
where T : DslVariableExtractor<T>
{
protected readonly string _variableName;
protected int? _matchNumber;
protected string _defaultValue;

public DslVariableExtractor(string name, string varName)
: base(name)
{
_variableName = varName;
}
}
}
16 changes: 13 additions & 3 deletions Abstracta.JmeterDsl/Core/Samplers/BaseSampler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using Abstracta.JmeterDsl.Core.TestElements;
using Abstracta.JmeterDsl.Core.ThreadGroups;

Expand All @@ -7,14 +7,24 @@ namespace Abstracta.JmeterDsl.Core.Samplers
/// <summary>
/// Hosts common logic to all samplers.
/// <br/>
/// In particular, it specifies that samplers are <see cref="IThreadGroupChild"/> and <see cref="TestElementContainer{T}"/> containing <see cref="ISamplerChild"/>.
/// In particular, it specifies that samplers are <see cref="IThreadGroupChild"/> and <see cref="TestElementContainer{T, C}"/> containing <see cref="ISamplerChild"/>.
/// For an example of an implementation of a sampler check <see cref="Http.DslHttpSampler"/>
/// </summary>
public abstract class BaseSampler : TestElementContainer<ISamplerChild>, IThreadGroupChild
public abstract class BaseSampler<T> : TestElementContainer<T, ISamplerChild>, IThreadGroupChild
where T : BaseSampler<T>
{
protected BaseSampler(string name)
: base(name, Array.Empty<ISamplerChild>())
{
}

/// <summary>
/// Allows specifying children test elements for the sampler, which allow for example extracting
/// information from response, alter request, assert response contents, etc.
/// </summary>
/// <param name="children">list of test elements to add as children of this sampler.</param>
/// <returns>the altered sampler to allow for fluent API usage.</returns>
public new T Children(params ISamplerChild[] children)
=> base.Children(children);
}
}
2 changes: 1 addition & 1 deletion Abstracta.JmeterDsl/Core/Samplers/DslDummySampler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Abstracta.JmeterDsl.Core.Samplers
/// and response time with random value between 50 and 500 milliseconds. Additionally, emulation of
/// response times (through sleeps) is disabled to speed up testing.
/// </summary>
public class DslDummySampler : BaseSampler
public class DslDummySampler : BaseSampler<DslDummySampler>
{
private readonly string _responseBody;
private bool? _successful;
Expand Down
66 changes: 66 additions & 0 deletions Abstracta.JmeterDsl/Core/TestElements/DslScopedTestElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace Abstracta.JmeterDsl.Core.TestElements
{
/// <summary>
/// Contains common logic for test elements that only process certain samples.
/// </summary>
/// <typeparam name="T">is the type of the test element that extends this class (to properly inherit fluent
/// API methods).</typeparam>
public abstract class DslScopedTestElement<T> : BaseTestElement, IMultiLevelTestElement
where T : DslScopedTestElement<T>
{
protected DslScope? _scope;
protected string _scopeVariable;

protected DslScopedTestElement(string name)
: base(name)
{
}

public enum DslScope
{
/// <summary>
/// Applies the regular extractor to all samples (main and sub samples).
/// </summary>
AllSamples,

/// <summary>
/// Applies the regular extractor only to main sample (sub samples, like redirects, are not
/// included).
/// </summary>
MainSample,

/// <summary>
/// Applies the regular extractor only to sub samples (redirects, embedded resources, etc.).
/// </summary>
SubSamples,
}

/// <summary>
/// Allows specifying if the element should be applied to main sample and/or sub samples.
/// <br/>
/// When not specified the element will only apply to main sample.
/// </summary>
/// <param name="scope">specifying to what sample result apply the element to.</param>
/// <returns>the DSL element for further configuration or usage.</returns>
/// <seealso cref="Scope"/>
public T Scope(DslScope scope)
{
_scope = scope;
return (T)this;
}

/// <summary>
/// Allows specifying that the element should be applied to the contents of a given JMeter
/// variable.
/// <br/>
/// This setting overrides any setting on scope and fieldToCheck.
/// </summary>
/// <param name="scopeVariable">specifies the name of the variable to apply the element to.</param>
/// <returns>the DSL element for further configuration or usage.</returns>
public T ScopeVariable(string scopeVariable)
{
_scopeVariable = scopeVariable;
return (T)this;
}
}
}
Loading

0 comments on commit 8cef5cc

Please sign in to comment.