Fix WithProbability logic (#1367)

* Fix WithProbability logic

* .

* FIX

* Update src/WireMock.Net.Minimal/Owin/MappingMatcher.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Stef Heyenrath
2025-10-22 10:16:59 +02:00
committed by GitHub
parent e3f3e0a8f2
commit 5885324dfb
5 changed files with 107 additions and 54 deletions

View File

@@ -267,7 +267,7 @@ namespace WireMock.Net.ConsoleApplication
} }
} }
public static void Run() public static async Task RunAsync()
{ {
//RunSse(); //RunSse();
//RunOnLocal(); //RunOnLocal();
@@ -290,25 +290,56 @@ namespace WireMock.Net.ConsoleApplication
var server = WireMockServer.Start(); var server = WireMockServer.Start();
//server
// .Given(Request.Create()
// .WithPath("todos")
// .UsingGet()
// )
// .RespondWith(Response.Create()
// .WithBodyAsJson(todos.Values)
// );
//server
// .Given(Request.Create()
// .UsingGet()
// .WithPath("todos")
// .WithParam("id")
// )
// .RespondWith(Response.Create()
// .WithBodyAsJson(rm => todos[int.Parse(rm.Query!["id"].ToString())])
// );
var pX = 0.80;
server server
.Given(Request.Create() .Given(Request.Create().UsingGet().WithPath("/p"))
.WithPath("todos") .WithProbability(pX)
.UsingGet() .RespondWith(Response.Create().WithStatusCode(200).WithBody("X"));
)
.RespondWith(Response.Create()
.WithBodyAsJson(todos.Values)
);
server server
.Given(Request.Create() .Given(Request.Create().UsingGet().WithPath("/p"))
.UsingGet() .RespondWith(Response.Create().WithStatusCode(200).WithBody("default"));
.WithPath("todos")
.WithParam("id")
)
.RespondWith(Response.Create()
.WithBodyAsJson(rm => todos[int.Parse(rm.Query!["id"].ToString())])
);
// Act
var requestUri = new Uri($"http://localhost:{server.Port}/p");
var c = server.CreateClient();
var xCount = 0;
var defaultCount = 0;
var tot = 1000;
for (var i = 0; i < tot; i++)
{
var response = await c.GetAsync(requestUri);
var value = await response.Content.ReadAsStringAsync();
if (value == "X")
{
xCount++;
}
else if (value == "default")
{
defaultCount++;
}
}
System.Console.WriteLine("X = {0} ; default = {1} ; pX = {2:0.00} ; valueX = {3:0.00}", xCount, defaultCount, pX, 1.0 * xCount / tot);
return;
using var httpAndHttpsWithPort = WireMockServer.Start(new WireMockServerSettings using var httpAndHttpsWithPort = WireMockServer.Start(new WireMockServerSettings
{ {
HostingScheme = HostingScheme.HttpAndHttps, HostingScheme = HostingScheme.HttpAndHttps,

View File

@@ -2,6 +2,7 @@
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using log4net; using log4net;
using log4net.Config; using log4net.Config;
using log4net.Repository; using log4net.Repository;
@@ -14,10 +15,10 @@ static class Program
private static readonly ILoggerRepository LogRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); private static readonly ILoggerRepository LogRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
private static readonly ILog Log = LogManager.GetLogger(typeof(Program)); private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
static void Main(params string[] args) static async Task Main(params string[] args)
{ {
XmlConfigurator.Configure(LogRepository, new FileInfo("log4net.config")); XmlConfigurator.Configure(LogRepository, new FileInfo("log4net.config"));
MainApp.Run(); await MainApp.RunAsync();
} }
} }

View File

@@ -9,16 +9,10 @@ using WireMock.Services;
namespace WireMock.Owin; namespace WireMock.Owin;
internal class MappingMatcher : IMappingMatcher internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDoubleBetween0And1 randomizerDoubleBetween0And1) : IMappingMatcher
{ {
private readonly IWireMockMiddlewareOptions _options; private readonly IWireMockMiddlewareOptions _options = Guard.NotNull(options);
private readonly IRandomizerDoubleBetween0And1 _randomizerDoubleBetween0And1; private readonly IRandomizerDoubleBetween0And1 _randomizerDoubleBetween0And1 = Guard.NotNull(randomizerDoubleBetween0And1);
public MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDoubleBetween0And1 randomizerDoubleBetween0And1)
{
_options = Guard.NotNull(options);
_randomizerDoubleBetween0And1 = Guard.NotNull(randomizerDoubleBetween0And1);
}
public (MappingMatcherResult? Match, MappingMatcherResult? Partial) FindBestMatch(RequestMessage request) public (MappingMatcherResult? Match, MappingMatcherResult? Partial) FindBestMatch(RequestMessage request)
{ {
@@ -28,7 +22,7 @@ internal class MappingMatcher : IMappingMatcher
var mappings = _options.Mappings.Values var mappings = _options.Mappings.Values
.Where(m => m.TimeSettings.IsValid()) .Where(m => m.TimeSettings.IsValid())
.Where(m => m.Probability is null || m.Probability <= _randomizerDoubleBetween0And1.Generate()) .Where(m => m.Probability is null || _randomizerDoubleBetween0And1.Generate() <= m.Probability)
.ToArray(); .ToArray();
foreach (var mapping in mappings) foreach (var mapping in mappings)
@@ -41,10 +35,10 @@ internal class MappingMatcher : IMappingMatcher
var exceptions = mappingMatcherResult.RequestMatchResult.MatchDetails var exceptions = mappingMatcherResult.RequestMatchResult.MatchDetails
.Where(md => md.Exception != null) .Where(md => md.Exception != null)
.Select(md => md.Exception) .Select(md => md.Exception!)
.ToArray(); .ToArray();
if (!exceptions.Any()) if (exceptions.Length == 0)
{ {
possibleMappings.Add(mappingMatcherResult); possibleMappings.Add(mappingMatcherResult);
} }
@@ -52,7 +46,7 @@ internal class MappingMatcher : IMappingMatcher
{ {
foreach (var ex in exceptions) foreach (var ex in exceptions)
{ {
LogException(mapping, ex!); LogException(mapping, ex);
} }
} }
} }
@@ -62,14 +56,16 @@ internal class MappingMatcher : IMappingMatcher
} }
} }
var partialMappings = possibleMappings var partialMatches = possibleMappings
.Where(pm => (pm.Mapping.IsAdminInterface && pm.RequestMatchResult.IsPerfectMatch) || !pm.Mapping.IsAdminInterface) .Where(pm => (pm.Mapping.IsAdminInterface && pm.RequestMatchResult.IsPerfectMatch) || !pm.Mapping.IsAdminInterface)
.OrderBy(m => m.RequestMatchResult) .OrderBy(m => m.RequestMatchResult)
.ThenBy(m => m.RequestMatchResult.TotalNumber) .ThenBy(m => m.RequestMatchResult.TotalNumber)
.ThenBy(m => m.Mapping.Priority) .ThenBy(m => m.Mapping.Priority)
.ThenByDescending(m => m.Mapping.Probability)
.ThenByDescending(m => m.Mapping.UpdatedAt) .ThenByDescending(m => m.Mapping.UpdatedAt)
.ToList(); .Where(pm => pm.RequestMatchResult.AverageTotalScore > 0.0)
var partialMatch = partialMappings.FirstOrDefault(pm => pm.RequestMatchResult.AverageTotalScore > 0.0); .ToArray();
var partialMatch = partialMatches.FirstOrDefault();
if (_options.AllowPartialMapping == true) if (_options.AllowPartialMapping == true)
{ {
@@ -78,7 +74,11 @@ internal class MappingMatcher : IMappingMatcher
var match = possibleMappings var match = possibleMappings
.Where(m => m.RequestMatchResult.IsPerfectMatch) .Where(m => m.RequestMatchResult.IsPerfectMatch)
.OrderBy(m => m.Mapping.Priority).ThenBy(m => m.RequestMatchResult).ThenByDescending(m => m.Mapping.UpdatedAt) .OrderBy(m => m.Mapping.Priority)
.ThenBy(m => m.RequestMatchResult)
.ThenBy(m => m.RequestMatchResult.TotalNumber)
.ThenByDescending(m => m.Mapping.Probability)
.ThenByDescending(m => m.Mapping.UpdatedAt)
.FirstOrDefault(); .FirstOrDefault();
return (match, partialMatch); return (match, partialMatch);

View File

@@ -9,7 +9,6 @@ using WireMock.Matchers.Request;
using WireMock.Models; using WireMock.Models;
using WireMock.Owin; using WireMock.Owin;
using WireMock.Services; using WireMock.Services;
using WireMock.Util;
using Xunit; using Xunit;
namespace WireMock.Net.Tests.Owin; namespace WireMock.Net.Tests.Owin;
@@ -26,7 +25,7 @@ public class MappingMatcherTests
_optionsMock = new Mock<IWireMockMiddlewareOptions>(); _optionsMock = new Mock<IWireMockMiddlewareOptions>();
_optionsMock.SetupAllProperties(); _optionsMock.SetupAllProperties();
_optionsMock.Setup(o => o.Mappings).Returns(new ConcurrentDictionary<Guid, IMapping>()); _optionsMock.Setup(o => o.Mappings).Returns(new ConcurrentDictionary<Guid, IMapping>());
_optionsMock.Setup(o => o.LogEntries).Returns(new ConcurrentObservableCollection<LogEntry>()); _optionsMock.Setup(o => o.LogEntries).Returns([]);
_optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary<string, ScenarioState>()); _optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary<string, ScenarioState>());
var loggerMock = new Mock<IWireMockLogger>(); var loggerMock = new Mock<IWireMockLogger>();
@@ -35,7 +34,7 @@ public class MappingMatcherTests
_optionsMock.Setup(o => o.Logger).Returns(loggerMock.Object); _optionsMock.Setup(o => o.Logger).Returns(loggerMock.Object);
_randomizerDoubleBetween0And1Mock = new Mock<IRandomizerDoubleBetween0And1>(); _randomizerDoubleBetween0And1Mock = new Mock<IRandomizerDoubleBetween0And1>();
_randomizerDoubleBetween0And1Mock.Setup(r => r.Generate()).Returns(0.0); _randomizerDoubleBetween0And1Mock.Setup(r => r.Generate()).Returns(0.5);
_sut = new MappingMatcher(_optionsMock.Object, _randomizerDoubleBetween0And1Mock.Object); _sut = new MappingMatcher(_optionsMock.Object, _randomizerDoubleBetween0And1Mock.Object);
} }
@@ -84,8 +83,8 @@ public class MappingMatcherTests
var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002");
var mappings = InitMappings var mappings = InitMappings
( (
(guid1, new[] { 0.1 }, null), (guid1, [0.1], null),
(guid2, new[] { 1.0 }, null) (guid2, [1.0], null)
); );
_optionsMock.Setup(o => o.Mappings).Returns(mappings); _optionsMock.Setup(o => o.Mappings).Returns(mappings);
@@ -112,8 +111,8 @@ public class MappingMatcherTests
var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002");
var mappings = InitMappings var mappings = InitMappings
( (
(guid1, new[] { 0.1 }, null), (guid1, [0.1], null),
(guid2, new[] { 0.9 }, null) (guid2, [0.9], null)
); );
_optionsMock.Setup(o => o.Mappings).Returns(mappings); _optionsMock.Setup(o => o.Mappings).Returns(mappings);
@@ -139,8 +138,8 @@ public class MappingMatcherTests
_optionsMock.SetupGet(o => o.AllowPartialMapping).Returns(true); _optionsMock.SetupGet(o => o.AllowPartialMapping).Returns(true);
var mappings = InitMappings( var mappings = InitMappings(
(guid1, new[] { 0.1 }, null), (guid1, [0.1], null),
(guid2, new[] { 0.9 }, null) (guid2, [0.9], null)
); );
_optionsMock.Setup(o => o.Mappings).Returns(mappings); _optionsMock.Setup(o => o.Mappings).Returns(mappings);
@@ -166,8 +165,8 @@ public class MappingMatcherTests
var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001");
var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002");
var mappings = InitMappings( var mappings = InitMappings(
(guid1, new[] { 1.0 }, null), (guid1, [1.0], null),
(guid2, new[] { 1.0, 1.0 }, null) (guid2, [1.0, 1.0], null)
); );
_optionsMock.Setup(o => o.Mappings).Returns(mappings); _optionsMock.Setup(o => o.Mappings).Returns(mappings);
@@ -187,15 +186,15 @@ public class MappingMatcherTests
} }
[Fact] [Fact]
public void MappingMatcher_FindBestMatch_WhenProbabilityFailsFirst_ShouldReturnSecondMatch() public void MappingMatcher_FindBestMatch_WhenProbabilityDoesNotMatch_ShouldReturnNormalMatch()
{ {
// Assign // Assign
var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); var withProbability = Guid.Parse("00000000-0000-0000-0000-000000000001");
var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); var noProbability = Guid.Parse("00000000-0000-0000-0000-000000000002");
var mappings = InitMappings var mappings = InitMappings
( (
(guid1, new[] { 1.0 }, 1.0), (withProbability, [1.0], 0.4),
(guid2, new[] { 1.0 }, null) (noProbability, [1.0], null)
); );
_optionsMock.Setup(o => o.Mappings).Returns(mappings); _optionsMock.Setup(o => o.Mappings).Returns(mappings);
@@ -206,8 +205,30 @@ public class MappingMatcherTests
// Assert // Assert
result.Match.Should().NotBeNull(); result.Match.Should().NotBeNull();
result.Match!.Mapping.Guid.Should().Be(guid2); result.Match!.Mapping.Guid.Should().Be(noProbability);
result.Match.RequestMatchResult.AverageTotalScore.Should().Be(1.0); }
[Fact]
public void MappingMatcher_FindBestMatch_WhenProbabilityDoesMatch_ShouldReturnProbabilityMatch()
{
// Assign
var withProbability = Guid.Parse("00000000-0000-0000-0000-000000000001");
var noProbability = Guid.Parse("00000000-0000-0000-0000-000000000002");
var mappings = InitMappings
(
(withProbability, [1.0], 0.6),
(noProbability, [1.0], null)
);
_optionsMock.Setup(o => o.Mappings).Returns(mappings);
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
// Act
var result = _sut.FindBestMatch(request);
// Assert
result.Match.Should().NotBeNull();
result.Match!.Mapping.Guid.Should().Be(withProbability);
} }
private static ConcurrentDictionary<Guid, IMapping> InitMappings(params (Guid guid, double[] scores, double? probability)[] matches) private static ConcurrentDictionary<Guid, IMapping> InitMappings(params (Guid guid, double[] scores, double? probability)[] matches)

View File

@@ -32,7 +32,7 @@ public partial class WireMockServerTests
var response = await server.CreateClient().GetAsync(requestUri).ConfigureAwait(false); var response = await server.CreateClient().GetAsync(requestUri).ConfigureAwait(false);
// Assert // Assert
Assert.True(new[] { HttpStatusCode.OK, HttpStatusCode.InternalServerError }.Contains(response.StatusCode)); Assert.Contains(response.StatusCode, [HttpStatusCode.OK, HttpStatusCode.InternalServerError]);
server.Stop(); server.Stop();
} }