From 5885324dfb20bee40e4de415e4a2ab0729d0a252 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 22 Oct 2025 10:16:59 +0200 Subject: [PATCH] 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> --- examples/WireMock.Net.Console.NET8/MainApp.cs | 63 ++++++++++++++----- examples/WireMock.Net.Console.NET8/Program.cs | 5 +- .../Owin/MappingMatcher.cs | 34 +++++----- .../Owin/MappingMatcherTests.cs | 57 +++++++++++------ .../WireMockServerTests.WithProbability.cs | 2 +- 5 files changed, 107 insertions(+), 54 deletions(-) diff --git a/examples/WireMock.Net.Console.NET8/MainApp.cs b/examples/WireMock.Net.Console.NET8/MainApp.cs index 942177dc..fa88484c 100644 --- a/examples/WireMock.Net.Console.NET8/MainApp.cs +++ b/examples/WireMock.Net.Console.NET8/MainApp.cs @@ -267,7 +267,7 @@ namespace WireMock.Net.ConsoleApplication } } - public static void Run() + public static async Task RunAsync() { //RunSse(); //RunOnLocal(); @@ -290,25 +290,56 @@ namespace WireMock.Net.ConsoleApplication 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 - .Given(Request.Create() - .WithPath("todos") - .UsingGet() - ) - .RespondWith(Response.Create() - .WithBodyAsJson(todos.Values) - ); + .Given(Request.Create().UsingGet().WithPath("/p")) + .WithProbability(pX) + .RespondWith(Response.Create().WithStatusCode(200).WithBody("X")); server - .Given(Request.Create() - .UsingGet() - .WithPath("todos") - .WithParam("id") - ) - .RespondWith(Response.Create() - .WithBodyAsJson(rm => todos[int.Parse(rm.Query!["id"].ToString())]) - ); + .Given(Request.Create().UsingGet().WithPath("/p")) + .RespondWith(Response.Create().WithStatusCode(200).WithBody("default")); + // 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 { HostingScheme = HostingScheme.HttpAndHttps, diff --git a/examples/WireMock.Net.Console.NET8/Program.cs b/examples/WireMock.Net.Console.NET8/Program.cs index 2a8ae4c4..9ca8b200 100644 --- a/examples/WireMock.Net.Console.NET8/Program.cs +++ b/examples/WireMock.Net.Console.NET8/Program.cs @@ -2,6 +2,7 @@ using System.IO; using System.Reflection; +using System.Threading.Tasks; using log4net; using log4net.Config; using log4net.Repository; @@ -14,10 +15,10 @@ static class Program private static readonly ILoggerRepository LogRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); 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")); - MainApp.Run(); + await MainApp.RunAsync(); } } \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs b/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs index 87861813..0180e865 100644 --- a/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs +++ b/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs @@ -9,16 +9,10 @@ using WireMock.Services; namespace WireMock.Owin; -internal class MappingMatcher : IMappingMatcher +internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDoubleBetween0And1 randomizerDoubleBetween0And1) : IMappingMatcher { - private readonly IWireMockMiddlewareOptions _options; - private readonly IRandomizerDoubleBetween0And1 _randomizerDoubleBetween0And1; - - public MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDoubleBetween0And1 randomizerDoubleBetween0And1) - { - _options = Guard.NotNull(options); - _randomizerDoubleBetween0And1 = Guard.NotNull(randomizerDoubleBetween0And1); - } + private readonly IWireMockMiddlewareOptions _options = Guard.NotNull(options); + private readonly IRandomizerDoubleBetween0And1 _randomizerDoubleBetween0And1 = Guard.NotNull(randomizerDoubleBetween0And1); public (MappingMatcherResult? Match, MappingMatcherResult? Partial) FindBestMatch(RequestMessage request) { @@ -28,7 +22,7 @@ internal class MappingMatcher : IMappingMatcher var mappings = _options.Mappings.Values .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(); foreach (var mapping in mappings) @@ -41,10 +35,10 @@ internal class MappingMatcher : IMappingMatcher var exceptions = mappingMatcherResult.RequestMatchResult.MatchDetails .Where(md => md.Exception != null) - .Select(md => md.Exception) + .Select(md => md.Exception!) .ToArray(); - if (!exceptions.Any()) + if (exceptions.Length == 0) { possibleMappings.Add(mappingMatcherResult); } @@ -52,7 +46,7 @@ internal class MappingMatcher : IMappingMatcher { 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) .OrderBy(m => m.RequestMatchResult) .ThenBy(m => m.RequestMatchResult.TotalNumber) .ThenBy(m => m.Mapping.Priority) + .ThenByDescending(m => m.Mapping.Probability) .ThenByDescending(m => m.Mapping.UpdatedAt) - .ToList(); - var partialMatch = partialMappings.FirstOrDefault(pm => pm.RequestMatchResult.AverageTotalScore > 0.0); + .Where(pm => pm.RequestMatchResult.AverageTotalScore > 0.0) + .ToArray(); + var partialMatch = partialMatches.FirstOrDefault(); if (_options.AllowPartialMapping == true) { @@ -78,7 +74,11 @@ internal class MappingMatcher : IMappingMatcher var match = possibleMappings .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(); return (match, partialMatch); diff --git a/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs b/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs index 48f325e1..e81bce1e 100644 --- a/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs +++ b/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs @@ -9,7 +9,6 @@ using WireMock.Matchers.Request; using WireMock.Models; using WireMock.Owin; using WireMock.Services; -using WireMock.Util; using Xunit; namespace WireMock.Net.Tests.Owin; @@ -26,7 +25,7 @@ public class MappingMatcherTests _optionsMock = new Mock(); _optionsMock.SetupAllProperties(); _optionsMock.Setup(o => o.Mappings).Returns(new ConcurrentDictionary()); - _optionsMock.Setup(o => o.LogEntries).Returns(new ConcurrentObservableCollection()); + _optionsMock.Setup(o => o.LogEntries).Returns([]); _optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary()); var loggerMock = new Mock(); @@ -35,7 +34,7 @@ public class MappingMatcherTests _optionsMock.Setup(o => o.Logger).Returns(loggerMock.Object); _randomizerDoubleBetween0And1Mock = new Mock(); - _randomizerDoubleBetween0And1Mock.Setup(r => r.Generate()).Returns(0.0); + _randomizerDoubleBetween0And1Mock.Setup(r => r.Generate()).Returns(0.5); _sut = new MappingMatcher(_optionsMock.Object, _randomizerDoubleBetween0And1Mock.Object); } @@ -84,8 +83,8 @@ public class MappingMatcherTests var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); var mappings = InitMappings ( - (guid1, new[] { 0.1 }, null), - (guid2, new[] { 1.0 }, null) + (guid1, [0.1], null), + (guid2, [1.0], null) ); _optionsMock.Setup(o => o.Mappings).Returns(mappings); @@ -112,8 +111,8 @@ public class MappingMatcherTests var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); var mappings = InitMappings ( - (guid1, new[] { 0.1 }, null), - (guid2, new[] { 0.9 }, null) + (guid1, [0.1], null), + (guid2, [0.9], null) ); _optionsMock.Setup(o => o.Mappings).Returns(mappings); @@ -139,8 +138,8 @@ public class MappingMatcherTests _optionsMock.SetupGet(o => o.AllowPartialMapping).Returns(true); var mappings = InitMappings( - (guid1, new[] { 0.1 }, null), - (guid2, new[] { 0.9 }, null) + (guid1, [0.1], null), + (guid2, [0.9], null) ); _optionsMock.Setup(o => o.Mappings).Returns(mappings); @@ -166,8 +165,8 @@ public class MappingMatcherTests var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); var mappings = InitMappings( - (guid1, new[] { 1.0 }, null), - (guid2, new[] { 1.0, 1.0 }, null) + (guid1, [1.0], null), + (guid2, [1.0, 1.0], null) ); _optionsMock.Setup(o => o.Mappings).Returns(mappings); @@ -187,15 +186,15 @@ public class MappingMatcherTests } [Fact] - public void MappingMatcher_FindBestMatch_WhenProbabilityFailsFirst_ShouldReturnSecondMatch() + public void MappingMatcher_FindBestMatch_WhenProbabilityDoesNotMatch_ShouldReturnNormalMatch() { // Assign - var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); - var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); + var withProbability = Guid.Parse("00000000-0000-0000-0000-000000000001"); + var noProbability = Guid.Parse("00000000-0000-0000-0000-000000000002"); var mappings = InitMappings ( - (guid1, new[] { 1.0 }, 1.0), - (guid2, new[] { 1.0 }, null) + (withProbability, [1.0], 0.4), + (noProbability, [1.0], null) ); _optionsMock.Setup(o => o.Mappings).Returns(mappings); @@ -206,8 +205,30 @@ public class MappingMatcherTests // Assert result.Match.Should().NotBeNull(); - result.Match!.Mapping.Guid.Should().Be(guid2); - result.Match.RequestMatchResult.AverageTotalScore.Should().Be(1.0); + result.Match!.Mapping.Guid.Should().Be(noProbability); + } + + [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 InitMappings(params (Guid guid, double[] scores, double? probability)[] matches) diff --git a/test/WireMock.Net.Tests/WireMockServerTests.WithProbability.cs b/test/WireMock.Net.Tests/WireMockServerTests.WithProbability.cs index 458d7149..e98eb0c5 100644 --- a/test/WireMock.Net.Tests/WireMockServerTests.WithProbability.cs +++ b/test/WireMock.Net.Tests/WireMockServerTests.WithProbability.cs @@ -32,7 +32,7 @@ public partial class WireMockServerTests var response = await server.CreateClient().GetAsync(requestUri).ConfigureAwait(false); // Assert - Assert.True(new[] { HttpStatusCode.OK, HttpStatusCode.InternalServerError }.Contains(response.StatusCode)); + Assert.Contains(response.StatusCode, [HttpStatusCode.OK, HttpStatusCode.InternalServerError]); server.Stop(); }