From 89d8e226dc9f285f94ce819e6efdb7be7c1b1bdd Mon Sep 17 00:00:00 2001 From: Josh Hiles <11314656+JoshHiles@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:41:07 +0000 Subject: [PATCH 1/5] feat: experiment.UseCustomOrdering() --- README.md | 19 ++- src/Scientist/IExperiment.cs | 9 +- src/Scientist/Internals/Experiment.cs | 19 ++- src/Scientist/Internals/ExperimentInstance.cs | 58 ++----- src/Scientist/Internals/ExperimentSettings.cs | 4 +- src/Scientist/NamedBehaviour.cs | 33 ++++ src/Scientist/Ordering/Ordering.cs | 34 ++++ src/Scientist/Scientist.csproj | 2 +- src/Scientist/packages.lock.json | 14 -- .../ExperimentTests/ExperimentTests.cs | 155 ++++++++++++++++++ test/Scientist.Test/Scientist.Test.csproj | 5 +- test/Scientist.Test/packages.lock.json | 75 ++++++--- 12 files changed, 336 insertions(+), 91 deletions(-) create mode 100644 src/Scientist/NamedBehaviour.cs create mode 100644 src/Scientist/Ordering/Ordering.cs create mode 100644 test/Scientist.Test/ExperimentTests/ExperimentTests.cs diff --git a/README.md b/README.md index 8a8aa43..b708ec0 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ public bool CanAccess(IUser user) Wrap a `Use` block around the code's original behavior, and wrap `Try` around the new behavior. Invoking `Scientist.Science` will always return whatever the `Use` block returns, but it does a bunch of stuff behind the scenes: * It decides whether or not to run the `Try` block, -* Randomizes the order in which `Use` and `Try` blocks are run, +* By default randomises the order in which `Use` and `Try` blocks are run, * Measures the durations of all behaviors, * Compares the result of `Try` to the result of `Use`, * Swallows (but records) any exceptions raised in the `Try` block, and @@ -341,6 +341,23 @@ public bool CanAccess(IUser user) } ``` +### Changing the order + +By default the library randomises the order in which behaviours (control & candidates) are ran however, under certain cirumstances you might want to run your own ordering so we provide a handy method `UseCustomOrdering`. +```csharp +scientist.Experiment(experimentName, experiment => +{ + experiment.UseCustomOrdering(Ordering.ControlFirst); + // ... +}); +``` +We also provide so pre-written alogrithms +```csharp +Ordering.Random +Ordering.ControlFirst +Ordering.ControlLast +``` + ## Alternatives Here are other implementations of Scientist available in different languages. diff --git a/src/Scientist/IExperiment.cs b/src/Scientist/IExperiment.cs index d4d037e..c490ae0 100644 --- a/src/Scientist/IExperiment.cs +++ b/src/Scientist/IExperiment.cs @@ -1,4 +1,6 @@ -using System; +using Github.Ordering; +using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace GitHub.Internals @@ -80,6 +82,8 @@ public interface IExperiment : IExperiment /// /// The delegate to execute void Ignore(Func block); + + void UseCustomOrdering(Func>, IReadOnlyList>> ordering); } /// @@ -138,6 +142,9 @@ public interface IExperimentAsync : IExperiment /// /// The delegate to execute void Ignore(Func> block); + + void UseCustomOrdering(CustomOrderer customOrdering); + } /// diff --git a/src/Scientist/Internals/Experiment.cs b/src/Scientist/Internals/Experiment.cs index bb7bd8d..f199933 100644 --- a/src/Scientist/Internals/Experiment.cs +++ b/src/Scientist/Internals/Experiment.cs @@ -1,4 +1,5 @@ -using System; +using Github.Ordering; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -28,6 +29,8 @@ private static readonly Action _alwaysThrow private readonly Dictionary _contexts = new Dictionary(); private readonly IResultPublisher _resultPublisher; + private CustomOrderer _customOrderer = behaviours => Task.FromResult(Ordering.Random(behaviours)); + public Experiment(string name, Func> enabled, int concurrentTasks, IResultPublisher resultPublisher) { if (concurrentTasks <= 0) @@ -130,7 +133,8 @@ internal ExperimentInstance Build() => RunIf = _runIf, Thrown = _thrown, ThrowOnMismatches = ThrowOnMismatches, - ResultPublisher = _resultPublisher + ResultPublisher = _resultPublisher, + CustomOrderer = _customOrderer }); public void Compare(Func comparison) @@ -156,5 +160,16 @@ public void BeforeRun(Func action) { _beforeRun = action; } + + + public void UseCustomOrdering(Func>, IReadOnlyList>> customOrdering) + { + _customOrderer = list => Task.FromResult(customOrdering(list)); + } + + public void UseCustomOrdering(CustomOrderer customOrdering) + { + _customOrderer = customOrdering; + } } } diff --git a/src/Scientist/Internals/ExperimentInstance.cs b/src/Scientist/Internals/ExperimentInstance.cs index 6e418bb..e18873c 100644 --- a/src/Scientist/Internals/ExperimentInstance.cs +++ b/src/Scientist/Internals/ExperimentInstance.cs @@ -1,4 +1,5 @@ -using System; +using Github.Ordering; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -16,7 +17,7 @@ internal class ExperimentInstance internal readonly string Name; internal readonly int ConcurrentTasks; - internal readonly List Behaviors; + internal readonly List> Behaviors; internal readonly Func Cleaner; internal readonly Func Comparator; internal readonly Func BeforeRun; @@ -27,19 +28,18 @@ internal class ExperimentInstance internal readonly Action Thrown; internal readonly bool ThrowOnMismatches; internal readonly IResultPublisher ResultPublisher; - - static Random _random = new Random(DateTimeOffset.UtcNow.Millisecond); - + internal readonly CustomOrderer CustomOrderer; + public ExperimentInstance(ExperimentSettings settings) { Name = settings.Name; - Behaviors = new List + Behaviors = new List> { - new NamedBehavior(ControlExperimentName, settings.Control), + new NamedBehaviour(ControlExperimentName, settings.Control), }; Behaviors.AddRange( - settings.Candidates.Select(c => new NamedBehavior(c.Key, c.Value))); + settings.Candidates.Select(c => new NamedBehaviour(c.Key, c.Value))); BeforeRun = settings.BeforeRun; Cleaner = settings.Cleaner; @@ -52,6 +52,7 @@ public ExperimentInstance(ExperimentSettings settings) Thrown = settings.Thrown; ThrowOnMismatches = settings.ThrowOnMismatches; ResultPublisher = settings.ResultPublisher; + CustomOrderer = settings.CustomOrderer; } public async Task Run() @@ -60,7 +61,7 @@ public async Task Run() if (!await ShouldExperimentRun().ConfigureAwait(false)) { // Run the control behavior. - return await Behaviors[0].Behavior().ConfigureAwait(false); + return await Behaviors[0].Behaviour().ConfigureAwait(false); } if (BeforeRun != null) @@ -68,12 +69,7 @@ public async Task Run() await BeforeRun().ConfigureAwait(false); } - // Randomize ordering... - NamedBehavior[] orderedBehaviors; - lock (_random) - { - orderedBehaviors = Behaviors.OrderBy(b => _random.Next()).ToArray(); - } + var orderedBehaviors = await CustomOrderer(Behaviors).ConfigureAwait(false); // Break tasks into batches of "ConcurrentTasks" size var observations = new List>(); @@ -84,7 +80,7 @@ public async Task Run() { return Observation.New( b.Name, - b.Behavior, + b.Behaviour, Comparator, Thrown, Cleaner); @@ -95,7 +91,7 @@ public async Task Run() } var controlObservation = observations.FirstOrDefault(o => o.Name == ControlExperimentName); - + var result = new Result(this, observations, controlObservation, Contexts); try @@ -115,7 +111,7 @@ public async Task Run() if (controlObservation.Thrown) throw controlObservation.Exception; return controlObservation.Value; } - + /// /// Does allow the experiment to run? /// @@ -149,7 +145,7 @@ public async Task IgnoreMismatchedObservation(Observation contr return false; } } - + /// /// Determine whether or not the experiment should run. /// @@ -167,29 +163,5 @@ async Task ShouldExperimentRun() return false; } } - - internal class NamedBehavior - { - public NamedBehavior(string name, Func behavior) - : this(name, () => Task.FromResult(behavior())) - { - } - - public NamedBehavior(string name, Func> behavior) - { - Behavior = behavior; - Name = name; - } - - /// - /// Gets the behavior to execute during an experiment. - /// - public Func> Behavior { get; } - - /// - /// Gets the name of the behavior. - /// - public string Name { get; } - } } } \ No newline at end of file diff --git a/src/Scientist/Internals/ExperimentSettings.cs b/src/Scientist/Internals/ExperimentSettings.cs index a824fa8..491cee2 100644 --- a/src/Scientist/Internals/ExperimentSettings.cs +++ b/src/Scientist/Internals/ExperimentSettings.cs @@ -1,4 +1,5 @@ -using System; +using Github.Ordering; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -26,5 +27,6 @@ internal class ExperimentSettings public bool ThrowOnMismatches { get; set; } public Action Thrown { get; set; } public IResultPublisher ResultPublisher { get; set; } + public CustomOrderer CustomOrderer { get; set; } } } diff --git a/src/Scientist/NamedBehaviour.cs b/src/Scientist/NamedBehaviour.cs new file mode 100644 index 0000000..588151e --- /dev/null +++ b/src/Scientist/NamedBehaviour.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; + +namespace GitHub +{ + public interface INamedBehaviour { + string Name { get; } + Func> Behaviour { get; } + } + public class NamedBehaviour: INamedBehaviour + { + public NamedBehaviour(string name, Func method) + : this(name, () => Task.FromResult(method())) + { + } + + public NamedBehaviour(string name, Func> method) + { + Behaviour = method; + Name = name; + } + + /// + /// Gets the name of the behavior. + /// + public string Name { get; } + + /// + /// Gets the behavior to execute during an experiment. + /// + public Func> Behaviour { get; } + } +} diff --git a/src/Scientist/Ordering/Ordering.cs b/src/Scientist/Ordering/Ordering.cs new file mode 100644 index 0000000..82ede90 --- /dev/null +++ b/src/Scientist/Ordering/Ordering.cs @@ -0,0 +1,34 @@ +using GitHub; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Github.Ordering +{ + public delegate Task>> CustomOrderer(IReadOnlyList> namedBehaviours); + + public class Ordering + { + public static IReadOnlyList> ControlFirst(IReadOnlyList> namedBehaviours) + { + return namedBehaviours.OrderByDescending(namedBehaviour => namedBehaviour.Name == "control").ToList(); + } + + public static IReadOnlyList> ControlLast(IReadOnlyList> namedBehaviours) + { + return ControlFirst(namedBehaviours).Reverse().ToList(); + } + + static Random _random = new Random(DateTimeOffset.UtcNow.Millisecond); + + public static IReadOnlyList> Random(IReadOnlyList> namedBehaviours) + { + lock (_random) + { + return namedBehaviours.OrderBy(b => _random.Next()).ToArray(); + } + } + } + +} diff --git a/src/Scientist/Scientist.csproj b/src/Scientist/Scientist.csproj index b12c6d5..812bad6 100644 --- a/src/Scientist/Scientist.csproj +++ b/src/Scientist/Scientist.csproj @@ -31,7 +31,7 @@ - + diff --git a/src/Scientist/packages.lock.json b/src/Scientist/packages.lock.json index d794fe5..c1dec41 100644 --- a/src/Scientist/packages.lock.json +++ b/src/Scientist/packages.lock.json @@ -2,15 +2,6 @@ "version": 1, "dependencies": { ".NETFramework,Version=v4.5.1": { - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net451": "1.0.3" - } - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -26,11 +17,6 @@ "resolved": "8.0.0", "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" }, - "Microsoft.NETFramework.ReferenceAssemblies.net451": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "vVPinxdLrwoX81ApbNIHDBI6qymQEy8eSOxDNBgKJtc2+cifnF0oT1U2d3EFx+V5O68yaqna2myZJNsgKCpVkA==" - }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "8.0.0", diff --git a/test/Scientist.Test/ExperimentTests/ExperimentTests.cs b/test/Scientist.Test/ExperimentTests/ExperimentTests.cs new file mode 100644 index 0000000..c770794 --- /dev/null +++ b/test/Scientist.Test/ExperimentTests/ExperimentTests.cs @@ -0,0 +1,155 @@ +using FluentAssertions; +using Github.Ordering; +using GitHub; +using GitHub.Internals; +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Linq; +using UnitTests; +using Xunit; + +public class ExperimentTests +{ + public class UseCustomOrdering + { + [Fact] + public void No_custom_ordering_specified_should_run_using_default_random() + { + const string experimentName = nameof(No_custom_ordering_specified_should_run_using_default_random); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var timesToRun = 10; + + var controlRanFirst = false; + var candidateRanFirst = false; + + for (int i = 0; i < timesToRun; i++) + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + + var result = scientist.Experiment(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + + var firstCall = mock.ReceivedCalls().First(); + + if (firstCall.GetMethodInfo().Name == "Control") + { + controlRanFirst = true; + } + + if (firstCall.GetMethodInfo().Name == "Candidate") + { + candidateRanFirst = true; + } + } + + controlRanFirst.Should().BeTrue($"Out of {timesToRun} runs, control should be seen to be ran first at least once"); + candidateRanFirst.Should().BeTrue($"Out of {timesToRun} runs, candidate should be seen to be ran first at least once"); + } + + [Fact] + public void ControlFirst_specified_should_run_first() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlFirst_specified_should_run_first); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = scientist.Experiment(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(Ordering.ControlFirst); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Control(); + mock.Received().Candidate(); + }); + } + + [Fact] + public void ControlLast_specified_should_run_last() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlLast_specified_should_run_last); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = scientist.Experiment(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(Ordering.ControlLast); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Candidate(); + mock.Received().Control(); + }); + } + + [Fact] + public void Passed_custom_ordering_method_runs() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlLast_specified_should_run_last); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = scientist.Experiment(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(SeededExperimentOrderer); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Candidate(); + mock.Received().Control(); + }); + } + + private static int _seed = 123; + + public static IReadOnlyList> SeededExperimentOrderer(IReadOnlyList> behaviours) + { + var random = new Random(_seed); + return behaviours.OrderBy(_ => random.Next()).ToList(); + } + } +} + diff --git a/test/Scientist.Test/Scientist.Test.csproj b/test/Scientist.Test/Scientist.Test.csproj index 530c363..efa114f 100644 --- a/test/Scientist.Test/Scientist.Test.csproj +++ b/test/Scientist.Test/Scientist.Test.csproj @@ -21,8 +21,9 @@ PreserveNewest - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Scientist.Test/packages.lock.json b/test/Scientist.Test/packages.lock.json index f4d397b..0497fb2 100644 --- a/test/Scientist.Test/packages.lock.json +++ b/test/Scientist.Test/packages.lock.json @@ -8,13 +8,22 @@ "resolved": "5.2.1", "contentHash": "wHARzQA695jwwKreOzNsq54KiGqKP38tv8hi8e2FXDEC/sA6BtrX90tVPDkOfVu13PbEzr00TCV8coikl+D1Iw==" }, + "FluentAssertions": { + "type": "Direct", + "requested": "[8.8.0, )", + "resolved": "8.8.0", + "contentHash": "m0kwcqBwvVel03FuMa7Ozo/oTaxYbjeNlcOhQFkyQpwX/8wks6RNl/Jnn58DCZVs6c2oG1RsCZw7HfKSaxLm3w==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.14.1, )", - "resolved": "17.14.1", - "contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==", + "requested": "[18.0.1, )", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", "dependencies": { - "Microsoft.CodeCoverage": "17.14.1" + "Microsoft.CodeCoverage": "18.0.1" } }, "NSubstitute": { @@ -40,17 +49,17 @@ }, "xunit.runner.visualstudio": { "type": "Direct", - "requested": "[3.1.4, )", - "resolved": "3.1.4", - "contentHash": "5mj99LvCqrq3CNi06xYdyIAXOEh+5b33F2nErCzI5zWiDdLHXiPXEWFSUAF8zlIv0ZWqjZNCwHTQeAPYbF3pCg==", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "17.13.0" } }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "17.14.1", - "contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==" + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", @@ -73,10 +82,18 @@ "System.Collections.Immutable": "1.5.0" } }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + }, "System.Threading.Tasks.Extensions": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==" + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } }, "xunit.abstractions": { "type": "Transitive", @@ -132,14 +149,20 @@ "System.Diagnostics.EventLog": "6.0.0" } }, + "FluentAssertions": { + "type": "Direct", + "requested": "[8.8.0, )", + "resolved": "8.8.0", + "contentHash": "m0kwcqBwvVel03FuMa7Ozo/oTaxYbjeNlcOhQFkyQpwX/8wks6RNl/Jnn58DCZVs6c2oG1RsCZw7HfKSaxLm3w==" + }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.14.1, )", - "resolved": "17.14.1", - "contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==", + "requested": "[18.0.1, )", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", "dependencies": { - "Microsoft.CodeCoverage": "17.14.1", - "Microsoft.TestPlatform.TestHost": "17.14.1" + "Microsoft.CodeCoverage": "18.0.1", + "Microsoft.TestPlatform.TestHost": "18.0.1" } }, "NSubstitute": { @@ -164,29 +187,29 @@ }, "xunit.runner.visualstudio": { "type": "Direct", - "requested": "[3.1.4, )", - "resolved": "3.1.4", - "contentHash": "5mj99LvCqrq3CNi06xYdyIAXOEh+5b33F2nErCzI5zWiDdLHXiPXEWFSUAF8zlIv0ZWqjZNCwHTQeAPYbF3pCg==" + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "17.14.1", - "contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==" + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "17.14.1", - "contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ==", + "resolved": "18.0.1", + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==", "dependencies": { "System.Reflection.Metadata": "8.0.0" } }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "17.14.1", - "contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==", + "resolved": "18.0.1", + "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.14.1", + "Microsoft.TestPlatform.ObjectModel": "18.0.1", "Newtonsoft.Json": "13.0.3" } }, From 558390cb9fececd09eea72b2d59488438cc9ddd0 Mon Sep 17 00:00:00 2001 From: Josh Hiles <11314656+JoshHiles@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:48:25 +0000 Subject: [PATCH 2/5] chore: add summaries for UseCustomOrdering --- src/Scientist/IExperiment.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Scientist/IExperiment.cs b/src/Scientist/IExperiment.cs index c490ae0..7d9a040 100644 --- a/src/Scientist/IExperiment.cs +++ b/src/Scientist/IExperiment.cs @@ -80,9 +80,13 @@ public interface IExperiment : IExperiment /// /// Defines the check to run to determine if mismatches should be ignored. /// - /// The delegate to execute + /// The delegate to execute. void Ignore(Func block); + /// + /// Defines the custom ordering to run on the behaviours + /// + /// The delgate to execute. void UseCustomOrdering(Func>, IReadOnlyList>> ordering); } @@ -140,11 +144,14 @@ public interface IExperimentAsync : IExperiment /// /// Defines the check to run to determine if mismatches should be ignored. /// - /// The delegate to execute + /// The delegate to execute. void Ignore(Func> block); + /// + /// Defines the custom ordering to run on the behaviours + /// + /// The delgate to execute. void UseCustomOrdering(CustomOrderer customOrdering); - } /// From 5eb1835bedd9cd7a01f8d3bd6fe938bb3dda3a3f Mon Sep 17 00:00:00 2001 From: Josh Hiles <11314656+JoshHiles@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:27:08 +0000 Subject: [PATCH 3/5] test: Add async tests for ordering --- .../ExperimentTests/ExperimentTests.cs | 155 ------------------ .../Ordering/OrderingAsyncTests.cs | 153 +++++++++++++++++ .../ExperimentTests/Ordering/OrderingTests.cs | 152 +++++++++++++++++ 3 files changed, 305 insertions(+), 155 deletions(-) delete mode 100644 test/Scientist.Test/ExperimentTests/ExperimentTests.cs create mode 100644 test/Scientist.Test/ExperimentTests/Ordering/OrderingAsyncTests.cs create mode 100644 test/Scientist.Test/ExperimentTests/Ordering/OrderingTests.cs diff --git a/test/Scientist.Test/ExperimentTests/ExperimentTests.cs b/test/Scientist.Test/ExperimentTests/ExperimentTests.cs deleted file mode 100644 index c770794..0000000 --- a/test/Scientist.Test/ExperimentTests/ExperimentTests.cs +++ /dev/null @@ -1,155 +0,0 @@ -using FluentAssertions; -using Github.Ordering; -using GitHub; -using GitHub.Internals; -using NSubstitute; -using System; -using System.Collections.Generic; -using System.Linq; -using UnitTests; -using Xunit; - -public class ExperimentTests -{ - public class UseCustomOrdering - { - [Fact] - public void No_custom_ordering_specified_should_run_using_default_random() - { - const string experimentName = nameof(No_custom_ordering_specified_should_run_using_default_random); - - var resultPublisher = new InMemoryResultPublisher(); - var scientist = new Scientist(resultPublisher); - - var timesToRun = 10; - - var controlRanFirst = false; - var candidateRanFirst = false; - - for (int i = 0; i < timesToRun; i++) - { - var mock = Substitute.For>(); - mock.Control().Returns(42); - mock.Candidate().Returns(42); - - var result = scientist.Experiment(experimentName, experiment => - { - experiment.ThrowOnMismatches = true; - experiment.Use(mock.Control); - experiment.Try("candidate", mock.Candidate); - }); - - result.Should().Be(42); - resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); - - var firstCall = mock.ReceivedCalls().First(); - - if (firstCall.GetMethodInfo().Name == "Control") - { - controlRanFirst = true; - } - - if (firstCall.GetMethodInfo().Name == "Candidate") - { - candidateRanFirst = true; - } - } - - controlRanFirst.Should().BeTrue($"Out of {timesToRun} runs, control should be seen to be ran first at least once"); - candidateRanFirst.Should().BeTrue($"Out of {timesToRun} runs, candidate should be seen to be ran first at least once"); - } - - [Fact] - public void ControlFirst_specified_should_run_first() - { - var mock = Substitute.For>(); - mock.Control().Returns(42); - mock.Candidate().Returns(42); - const string experimentName = nameof(ControlFirst_specified_should_run_first); - - var resultPublisher = new InMemoryResultPublisher(); - var scientist = new Scientist(resultPublisher); - - var result = scientist.Experiment(experimentName, experiment => - { - experiment.ThrowOnMismatches = true; - experiment.UseCustomOrdering(Ordering.ControlFirst); - experiment.Use(mock.Control); - experiment.Try("candidate", mock.Candidate); - }); - - result.Should().Be(42); - resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); - Received.InOrder(() => - { - mock.Received().Control(); - mock.Received().Candidate(); - }); - } - - [Fact] - public void ControlLast_specified_should_run_last() - { - var mock = Substitute.For>(); - mock.Control().Returns(42); - mock.Candidate().Returns(42); - const string experimentName = nameof(ControlLast_specified_should_run_last); - - var resultPublisher = new InMemoryResultPublisher(); - var scientist = new Scientist(resultPublisher); - - var result = scientist.Experiment(experimentName, experiment => - { - experiment.ThrowOnMismatches = true; - experiment.UseCustomOrdering(Ordering.ControlLast); - experiment.Use(mock.Control); - experiment.Try("candidate", mock.Candidate); - }); - - result.Should().Be(42); - resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); - Received.InOrder(() => - { - mock.Received().Candidate(); - mock.Received().Control(); - }); - } - - [Fact] - public void Passed_custom_ordering_method_runs() - { - var mock = Substitute.For>(); - mock.Control().Returns(42); - mock.Candidate().Returns(42); - const string experimentName = nameof(ControlLast_specified_should_run_last); - - var resultPublisher = new InMemoryResultPublisher(); - var scientist = new Scientist(resultPublisher); - - var result = scientist.Experiment(experimentName, experiment => - { - experiment.ThrowOnMismatches = true; - experiment.UseCustomOrdering(SeededExperimentOrderer); - experiment.Use(mock.Control); - experiment.Try("candidate", mock.Candidate); - }); - - result.Should().Be(42); - resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); - Received.InOrder(() => - { - mock.Received().Candidate(); - mock.Received().Control(); - }); - } - - private static int _seed = 123; - - public static IReadOnlyList> SeededExperimentOrderer(IReadOnlyList> behaviours) - { - var random = new Random(_seed); - return behaviours.OrderBy(_ => random.Next()).ToList(); - } - } -} - diff --git a/test/Scientist.Test/ExperimentTests/Ordering/OrderingAsyncTests.cs b/test/Scientist.Test/ExperimentTests/Ordering/OrderingAsyncTests.cs new file mode 100644 index 0000000..c5420d7 --- /dev/null +++ b/test/Scientist.Test/ExperimentTests/Ordering/OrderingAsyncTests.cs @@ -0,0 +1,153 @@ +using FluentAssertions; +using Github.Ordering; +using GitHub; +using GitHub.Internals; +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnitTests; +using Xunit; + +public class OrderingAsyncTests +{ + [Fact] + public async Task No_custom_ordering_specified_should_run_using_default_random() + { + const string experimentName = nameof(No_custom_ordering_specified_should_run_using_default_random); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var timesToRun = 10; + + var controlRanFirst = false; + var candidateRanFirst = false; + + for (int i = 0; i < timesToRun; i++) + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + + var result = await scientist.ExperimentAsync(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + + var firstCall = mock.ReceivedCalls().First(); + + if (firstCall.GetMethodInfo().Name == "Control") + { + controlRanFirst = true; + } + + if (firstCall.GetMethodInfo().Name == "Candidate") + { + candidateRanFirst = true; + } + } + + controlRanFirst.Should().BeTrue($"Out of {timesToRun} runs, control should be seen to be ran first at least once"); + candidateRanFirst.Should().BeTrue($"Out of {timesToRun} runs, candidate should be seen to be ran first at least once"); + } + + [Fact] + public async Task ControlFirst_specified_should_run_first() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlFirst_specified_should_run_first); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = await scientist.ExperimentAsync(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(behaviours => Task.FromResult(Ordering.ControlFirst(behaviours))); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Control(); + mock.Received().Candidate(); + }); + } + + [Fact] + public async Task ControlLast_specified_should_run_last() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlLast_specified_should_run_last); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = await scientist.ExperimentAsync(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(behaviours => Task.FromResult(Ordering.ControlLast(behaviours))); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Candidate(); + mock.Received().Control(); + }); + } + + [Fact] + public async Task Passed_custom_ordering_method_runs() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlLast_specified_should_run_last); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = await scientist.ExperimentAsync(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(SeededExperimentOrderer); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Candidate(); + mock.Received().Control(); + }); + } + + private static int _seed = 123; + + public static async Task>> SeededExperimentOrderer(IReadOnlyList> behaviours) + { + var random = new Random(_seed); + return behaviours.OrderBy(_ => random.Next()).ToList(); + } +} + diff --git a/test/Scientist.Test/ExperimentTests/Ordering/OrderingTests.cs b/test/Scientist.Test/ExperimentTests/Ordering/OrderingTests.cs new file mode 100644 index 0000000..9ba337e --- /dev/null +++ b/test/Scientist.Test/ExperimentTests/Ordering/OrderingTests.cs @@ -0,0 +1,152 @@ +using FluentAssertions; +using Github.Ordering; +using GitHub; +using GitHub.Internals; +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Linq; +using UnitTests; +using Xunit; + +public class OrderingTests +{ + [Fact] + public void No_custom_ordering_specified_should_run_using_default_random() + { + const string experimentName = nameof(No_custom_ordering_specified_should_run_using_default_random); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var timesToRun = 10; + + var controlRanFirst = false; + var candidateRanFirst = false; + + for (int i = 0; i < timesToRun; i++) + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + + var result = scientist.Experiment(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + + var firstCall = mock.ReceivedCalls().First(); + + if (firstCall.GetMethodInfo().Name == "Control") + { + controlRanFirst = true; + } + + if (firstCall.GetMethodInfo().Name == "Candidate") + { + candidateRanFirst = true; + } + } + + controlRanFirst.Should().BeTrue($"Out of {timesToRun} runs, control should be seen to be ran first at least once"); + candidateRanFirst.Should().BeTrue($"Out of {timesToRun} runs, candidate should be seen to be ran first at least once"); + } + + [Fact] + public void ControlFirst_specified_should_run_first() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlFirst_specified_should_run_first); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = scientist.Experiment(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(Ordering.ControlFirst); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Control(); + mock.Received().Candidate(); + }); + } + + [Fact] + public void ControlLast_specified_should_run_last() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlLast_specified_should_run_last); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = scientist.Experiment(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(Ordering.ControlLast); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Candidate(); + mock.Received().Control(); + }); + } + + [Fact] + public void Passed_custom_ordering_method_runs() + { + var mock = Substitute.For>(); + mock.Control().Returns(42); + mock.Candidate().Returns(42); + const string experimentName = nameof(ControlLast_specified_should_run_last); + + var resultPublisher = new InMemoryResultPublisher(); + var scientist = new Scientist(resultPublisher); + + var result = scientist.Experiment(experimentName, experiment => + { + experiment.ThrowOnMismatches = true; + experiment.UseCustomOrdering(SeededExperimentOrderer); + experiment.Use(mock.Control); + experiment.Try("candidate", mock.Candidate); + }); + + result.Should().Be(42); + resultPublisher.Results(experimentName).First().Matched.Should().BeTrue(); + Received.InOrder(() => + { + mock.Received().Candidate(); + mock.Received().Control(); + }); + } + + private static int _seed = 123; + + public static IReadOnlyList> SeededExperimentOrderer(IReadOnlyList> behaviours) + { + var random = new Random(_seed); + return behaviours.OrderBy(_ => random.Next()).ToList(); + } +} + From a5bf756cea991d1f94215100c315b411fc78677e Mon Sep 17 00:00:00 2001 From: Josh Hiles <11314656+JoshHiles@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:32:49 +0000 Subject: [PATCH 4/5] docs: include information on DIY custom order method --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b708ec0..6b457b5 100644 --- a/README.md +++ b/README.md @@ -351,12 +351,30 @@ scientist.Experiment(experimentName, experiment => // ... }); ``` -We also provide so pre-written alogrithms +There are some pre-written alogrithms ```csharp Ordering.Random Ordering.ControlFirst Ordering.ControlLast ``` +But if you need to rock your own then you can write something like below +```csharp + private static int _seed = 123; + + public static IReadOnlyList> SeededExperimentOrderer(IReadOnlyList> behaviours) + { + var random = new Random(_seed); + return behaviours.OrderBy(_ => random.Next()).ToList(); + } + +// ... + +scientist.Experiment(experimentName, experiment => +{ + experiment.UseCustomOrdering(SeededExperimentOrderer); + // ... +}); +``` ## Alternatives From 8f6b76b975958e3a802cb11e09aaa44b2f30b392 Mon Sep 17 00:00:00 2001 From: Josh Hiles <11314656+JoshHiles@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:42:09 +0000 Subject: [PATCH 5/5] refactor: behaviour to behavior --- README.md | 6 +++--- src/Scientist/IExperiment.cs | 6 +++--- src/Scientist/Internals/Experiment.cs | 4 ++-- src/Scientist/Internals/ExperimentInstance.cs | 12 ++++++------ .../{NamedBehaviour.cs => NamedBehavior.cs} | 17 +++++++++-------- src/Scientist/Ordering/Ordering.cs | 14 +++++++------- .../Ordering/OrderingAsyncTests.cs | 8 ++++---- .../ExperimentTests/Ordering/OrderingTests.cs | 4 ++-- 8 files changed, 36 insertions(+), 35 deletions(-) rename src/Scientist/{NamedBehaviour.cs => NamedBehavior.cs} (56%) diff --git a/README.md b/README.md index 6b457b5..e09e9a7 100644 --- a/README.md +++ b/README.md @@ -343,7 +343,7 @@ public bool CanAccess(IUser user) ### Changing the order -By default the library randomises the order in which behaviours (control & candidates) are ran however, under certain cirumstances you might want to run your own ordering so we provide a handy method `UseCustomOrdering`. +By default the library randomises the order in which behaviors (control & candidates) are ran however, under certain cirumstances you might want to run your own ordering so we provide a handy method `UseCustomOrdering`. ```csharp scientist.Experiment(experimentName, experiment => { @@ -361,10 +361,10 @@ But if you need to rock your own then you can write something like below ```csharp private static int _seed = 123; - public static IReadOnlyList> SeededExperimentOrderer(IReadOnlyList> behaviours) + public static IReadOnlyList> SeededExperimentOrderer(IReadOnlyList> behaviors) { var random = new Random(_seed); - return behaviours.OrderBy(_ => random.Next()).ToList(); + return behaviors.OrderBy(_ => random.Next()).ToList(); } // ... diff --git a/src/Scientist/IExperiment.cs b/src/Scientist/IExperiment.cs index 7d9a040..52924d6 100644 --- a/src/Scientist/IExperiment.cs +++ b/src/Scientist/IExperiment.cs @@ -84,10 +84,10 @@ public interface IExperiment : IExperiment void Ignore(Func block); /// - /// Defines the custom ordering to run on the behaviours + /// Defines the custom ordering to run on the behaviors /// /// The delgate to execute. - void UseCustomOrdering(Func>, IReadOnlyList>> ordering); + void UseCustomOrdering(Func>, IReadOnlyList>> ordering); } /// @@ -148,7 +148,7 @@ public interface IExperimentAsync : IExperiment void Ignore(Func> block); /// - /// Defines the custom ordering to run on the behaviours + /// Defines the custom ordering to run on the behaviors /// /// The delgate to execute. void UseCustomOrdering(CustomOrderer customOrdering); diff --git a/src/Scientist/Internals/Experiment.cs b/src/Scientist/Internals/Experiment.cs index f199933..90bd97c 100644 --- a/src/Scientist/Internals/Experiment.cs +++ b/src/Scientist/Internals/Experiment.cs @@ -29,7 +29,7 @@ private static readonly Action _alwaysThrow private readonly Dictionary _contexts = new Dictionary(); private readonly IResultPublisher _resultPublisher; - private CustomOrderer _customOrderer = behaviours => Task.FromResult(Ordering.Random(behaviours)); + private CustomOrderer _customOrderer = behaviors => Task.FromResult(Ordering.Random(behaviors)); public Experiment(string name, Func> enabled, int concurrentTasks, IResultPublisher resultPublisher) { @@ -162,7 +162,7 @@ public void BeforeRun(Func action) } - public void UseCustomOrdering(Func>, IReadOnlyList>> customOrdering) + public void UseCustomOrdering(Func>, IReadOnlyList>> customOrdering) { _customOrderer = list => Task.FromResult(customOrdering(list)); } diff --git a/src/Scientist/Internals/ExperimentInstance.cs b/src/Scientist/Internals/ExperimentInstance.cs index e18873c..d3d9b4d 100644 --- a/src/Scientist/Internals/ExperimentInstance.cs +++ b/src/Scientist/Internals/ExperimentInstance.cs @@ -17,7 +17,7 @@ internal class ExperimentInstance internal readonly string Name; internal readonly int ConcurrentTasks; - internal readonly List> Behaviors; + internal readonly List> Behaviors; internal readonly Func Cleaner; internal readonly Func Comparator; internal readonly Func BeforeRun; @@ -34,12 +34,12 @@ public ExperimentInstance(ExperimentSettings settings) { Name = settings.Name; - Behaviors = new List> + Behaviors = new List> { - new NamedBehaviour(ControlExperimentName, settings.Control), + new NamedBehavior(ControlExperimentName, settings.Control), }; Behaviors.AddRange( - settings.Candidates.Select(c => new NamedBehaviour(c.Key, c.Value))); + settings.Candidates.Select(c => new NamedBehavior(c.Key, c.Value))); BeforeRun = settings.BeforeRun; Cleaner = settings.Cleaner; @@ -61,7 +61,7 @@ public async Task Run() if (!await ShouldExperimentRun().ConfigureAwait(false)) { // Run the control behavior. - return await Behaviors[0].Behaviour().ConfigureAwait(false); + return await Behaviors[0].Behavior().ConfigureAwait(false); } if (BeforeRun != null) @@ -80,7 +80,7 @@ public async Task Run() { return Observation.New( b.Name, - b.Behaviour, + b.Behavior, Comparator, Thrown, Cleaner); diff --git a/src/Scientist/NamedBehaviour.cs b/src/Scientist/NamedBehavior.cs similarity index 56% rename from src/Scientist/NamedBehaviour.cs rename to src/Scientist/NamedBehavior.cs index 588151e..aed8834 100644 --- a/src/Scientist/NamedBehaviour.cs +++ b/src/Scientist/NamedBehavior.cs @@ -3,20 +3,21 @@ namespace GitHub { - public interface INamedBehaviour { + public interface INamedBehavior + { string Name { get; } - Func> Behaviour { get; } + Func> Behavior { get; } } - public class NamedBehaviour: INamedBehaviour + public class NamedBehavior : INamedBehavior { - public NamedBehaviour(string name, Func method) + public NamedBehavior(string name, Func method) : this(name, () => Task.FromResult(method())) { - } + } - public NamedBehaviour(string name, Func> method) + public NamedBehavior(string name, Func> method) { - Behaviour = method; + Behavior = method; Name = name; } @@ -28,6 +29,6 @@ public NamedBehaviour(string name, Func> method) /// /// Gets the behavior to execute during an experiment. /// - public Func> Behaviour { get; } + public Func> Behavior { get; } } } diff --git a/src/Scientist/Ordering/Ordering.cs b/src/Scientist/Ordering/Ordering.cs index 82ede90..067a17c 100644 --- a/src/Scientist/Ordering/Ordering.cs +++ b/src/Scientist/Ordering/Ordering.cs @@ -6,27 +6,27 @@ namespace Github.Ordering { - public delegate Task>> CustomOrderer(IReadOnlyList> namedBehaviours); + public delegate Task>> CustomOrderer(IReadOnlyList> namedBehaviors); public class Ordering { - public static IReadOnlyList> ControlFirst(IReadOnlyList> namedBehaviours) + public static IReadOnlyList> ControlFirst(IReadOnlyList> namedBehaviors) { - return namedBehaviours.OrderByDescending(namedBehaviour => namedBehaviour.Name == "control").ToList(); + return namedBehaviors.OrderByDescending(namedBehavior => namedBehavior.Name == "control").ToList(); } - public static IReadOnlyList> ControlLast(IReadOnlyList> namedBehaviours) + public static IReadOnlyList> ControlLast(IReadOnlyList> namedBehaviors) { - return ControlFirst(namedBehaviours).Reverse().ToList(); + return ControlFirst(namedBehaviors).Reverse().ToList(); } static Random _random = new Random(DateTimeOffset.UtcNow.Millisecond); - public static IReadOnlyList> Random(IReadOnlyList> namedBehaviours) + public static IReadOnlyList> Random(IReadOnlyList> namedBehaviors) { lock (_random) { - return namedBehaviours.OrderBy(b => _random.Next()).ToArray(); + return namedBehaviors.OrderBy(b => _random.Next()).ToArray(); } } } diff --git a/test/Scientist.Test/ExperimentTests/Ordering/OrderingAsyncTests.cs b/test/Scientist.Test/ExperimentTests/Ordering/OrderingAsyncTests.cs index c5420d7..0785ac5 100644 --- a/test/Scientist.Test/ExperimentTests/Ordering/OrderingAsyncTests.cs +++ b/test/Scientist.Test/ExperimentTests/Ordering/OrderingAsyncTests.cs @@ -72,7 +72,7 @@ public async Task ControlFirst_specified_should_run_first() var result = await scientist.ExperimentAsync(experimentName, experiment => { experiment.ThrowOnMismatches = true; - experiment.UseCustomOrdering(behaviours => Task.FromResult(Ordering.ControlFirst(behaviours))); + experiment.UseCustomOrdering(behaviors => Task.FromResult(Ordering.ControlFirst(behaviors))); experiment.Use(mock.Control); experiment.Try("candidate", mock.Candidate); }); @@ -100,7 +100,7 @@ public async Task ControlLast_specified_should_run_last() var result = await scientist.ExperimentAsync(experimentName, experiment => { experiment.ThrowOnMismatches = true; - experiment.UseCustomOrdering(behaviours => Task.FromResult(Ordering.ControlLast(behaviours))); + experiment.UseCustomOrdering(behaviors => Task.FromResult(Ordering.ControlLast(behaviors))); experiment.Use(mock.Control); experiment.Try("candidate", mock.Candidate); }); @@ -144,10 +144,10 @@ public async Task Passed_custom_ordering_method_runs() private static int _seed = 123; - public static async Task>> SeededExperimentOrderer(IReadOnlyList> behaviours) + public static async Task>> SeededExperimentOrderer(IReadOnlyList> behaviors) { var random = new Random(_seed); - return behaviours.OrderBy(_ => random.Next()).ToList(); + return behaviors.OrderBy(_ => random.Next()).ToList(); } } diff --git a/test/Scientist.Test/ExperimentTests/Ordering/OrderingTests.cs b/test/Scientist.Test/ExperimentTests/Ordering/OrderingTests.cs index 9ba337e..aa43594 100644 --- a/test/Scientist.Test/ExperimentTests/Ordering/OrderingTests.cs +++ b/test/Scientist.Test/ExperimentTests/Ordering/OrderingTests.cs @@ -143,10 +143,10 @@ public void Passed_custom_ordering_method_runs() private static int _seed = 123; - public static IReadOnlyList> SeededExperimentOrderer(IReadOnlyList> behaviours) + public static IReadOnlyList> SeededExperimentOrderer(IReadOnlyList> behaviors) { var random = new Random(_seed); - return behaviours.OrderBy(_ => random.Next()).ToList(); + return behaviors.OrderBy(_ => random.Next()).ToList(); } }