From db1496b6570f6751f4910ba35c3913f1f21b9059 Mon Sep 17 00:00:00 2001 From: Joshua Hiles Date: Mon, 8 Jan 2024 18:41:56 +0000 Subject: [PATCH 1/6] docs(security): Use new GitHub Security Advisory --- SECURITY.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index c22fe3e..04ed689 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,4 @@ -# Security Policy +# Security ## Supported Versions @@ -9,9 +9,14 @@ ## Reporting a Vulnerability -Security issues and bugs should be reported privately. +To report a security issue, please use the GitHub Security Advisory [Report a Vulnerability](https://github.com/scientistproject/Scientist.net/security/advisories/new) tab. -> The best approach at the moment is to message one of the contributors on https://gitter.im/scientistproject/community privately -> If you dont receive a reply within a couple of days please send the message onto another member +We will: -Please do not open issues for anything you think might have a security implication. +- Treat the vulnerability as a security issue rather than a simple bug, both in our response and our disclosure. For example, we will explicitly mention that the issue is a security vulnerability in our release notes. +- Acknowledge receipt of the vulnerability report as quickly as possible, even if no immediate resources are available for investigation. +- Involve the vulnerability reporter when we verify the impact and veracity of the report. +- Remediate the issue in a way that we see fit, taking any concerns and advice provided by the vulnerability reporter into careful consideration. +- Always acknowledge the vulnerability reporter when we credit the discovery. +- Aim to publish a fix as soon as we can. +- Ensure that we make the wider ecosystem aware of the issue and its remediation when you disclose the vulnerability. From 4068dfc56a3a1bced44cfa4b8cc3255dd0eb11fd Mon Sep 17 00:00:00 2001 From: Joshua Hiles Date: Mon, 8 Jan 2024 18:42:20 +0000 Subject: [PATCH 2/6] refactor(solution): Update name --- Scientist.net.sln => Scientist.sln | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) rename Scientist.net.sln => Scientist.sln (72%) diff --git a/Scientist.net.sln b/Scientist.sln similarity index 72% rename from Scientist.net.sln rename to Scientist.sln index 6edb64a..0ad481d 100644 --- a/Scientist.net.sln +++ b/Scientist.sln @@ -5,7 +5,9 @@ VisualStudioVersion = 17.0.31512.422 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scientist", "Scientist\Scientist.csproj", "{8794D5E1-EC60-48CD-A834-63B3531D90B3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scientist.Test", "Scientist.Test\Scientist.Test.csproj", "{8087793F-0EC4-4085-9C18-91D991FFE3DF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scientist.Test", "Scientist.Test\Scientist.Test.csproj", "{8087793F-0EC4-4085-9C18-91D991FFE3DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scientist.Benchmark", "Scientist.Benchmark\Scientist.Benchmark.csproj", "{06AAA6F1-DF1C-4486-AEB8-80005DB22065}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,6 +23,10 @@ Global {8087793F-0EC4-4085-9C18-91D991FFE3DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {8087793F-0EC4-4085-9C18-91D991FFE3DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {8087793F-0EC4-4085-9C18-91D991FFE3DF}.Release|Any CPU.Build.0 = Release|Any CPU + {06AAA6F1-DF1C-4486-AEB8-80005DB22065}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06AAA6F1-DF1C-4486-AEB8-80005DB22065}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06AAA6F1-DF1C-4486-AEB8-80005DB22065}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06AAA6F1-DF1C-4486-AEB8-80005DB22065}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From f4ac8abb81a2d9add638582bd8d98f0d7cb7d75e Mon Sep 17 00:00:00 2001 From: Joshua Hiles Date: Mon, 8 Jan 2024 18:42:34 +0000 Subject: [PATCH 3/6] test(benchmarking): Add benchmarking project --- Scientist.Benchmark/ExperimentBenchmarks.cs | 29 +++++++++++++++++++ Scientist.Benchmark/Program.cs | 6 ++++ .../Scientist.Benchmark.csproj | 18 ++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 Scientist.Benchmark/ExperimentBenchmarks.cs create mode 100644 Scientist.Benchmark/Program.cs create mode 100644 Scientist.Benchmark/Scientist.Benchmark.csproj diff --git a/Scientist.Benchmark/ExperimentBenchmarks.cs b/Scientist.Benchmark/ExperimentBenchmarks.cs new file mode 100644 index 0000000..20952f6 --- /dev/null +++ b/Scientist.Benchmark/ExperimentBenchmarks.cs @@ -0,0 +1,29 @@ +using BenchmarkDotNet.Attributes; + +namespace Scientist.Benchmark +{ + [MemoryDiagnoser] + public class ExperimentBenchmarks + { + readonly Experiment experiment; + + public ExperimentBenchmarks() + { + experiment = new Experiment(name: "Instance", control: FortyTwo); + } + + [Benchmark] + public int Static() + { + return new Experiment(name: "Static", control: FortyTwo).Run(); + } + + [Benchmark(Baseline = false)] + public int Instance() + { + return experiment.Run(); + } + + private static int FortyTwo() => 42; + } +} diff --git a/Scientist.Benchmark/Program.cs b/Scientist.Benchmark/Program.cs new file mode 100644 index 0000000..b356bdc --- /dev/null +++ b/Scientist.Benchmark/Program.cs @@ -0,0 +1,6 @@ +// See https://aka.ms/new-console-template for more information + +using BenchmarkDotNet.Running; +using Scientist.Benchmark; + +var summary = BenchmarkRunner.Run(); diff --git a/Scientist.Benchmark/Scientist.Benchmark.csproj b/Scientist.Benchmark/Scientist.Benchmark.csproj new file mode 100644 index 0000000..76487a8 --- /dev/null +++ b/Scientist.Benchmark/Scientist.Benchmark.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + From fce5fb52c7a6a5992a8f06bbb4e2d92ba89a688c Mon Sep 17 00:00:00 2001 From: Joshua Hiles Date: Mon, 8 Jan 2024 18:43:11 +0000 Subject: [PATCH 4/6] test: start of rewriting tests for fluent --- Scientist.Test/Comparison/ComparisonTests.cs | 60 ++ Scientist.Test/Experiment/ExperimentTests.cs | 111 +++ Scientist.Test/GlobalUsings.cs | 2 + Scientist.Test/Helpers/ComplexResult.cs | 22 + Scientist.Test/Helpers/ITestClass.cs | 14 + Scientist.Test/Publishing/PublishingTests.cs | 14 + Scientist.Test/RunIf/RunIfTests.cs | 54 ++ Scientist.Test/Scientist.Test.csproj | 23 +- Scientist.Test/Scientist.cs | 14 - Scientist.Test/packages.lock.json | 800 +++++-------------- 10 files changed, 488 insertions(+), 626 deletions(-) create mode 100644 Scientist.Test/Comparison/ComparisonTests.cs create mode 100644 Scientist.Test/Experiment/ExperimentTests.cs create mode 100644 Scientist.Test/GlobalUsings.cs create mode 100644 Scientist.Test/Helpers/ComplexResult.cs create mode 100644 Scientist.Test/Helpers/ITestClass.cs create mode 100644 Scientist.Test/Publishing/PublishingTests.cs create mode 100644 Scientist.Test/RunIf/RunIfTests.cs delete mode 100644 Scientist.Test/Scientist.cs diff --git a/Scientist.Test/Comparison/ComparisonTests.cs b/Scientist.Test/Comparison/ComparisonTests.cs new file mode 100644 index 0000000..4740825 --- /dev/null +++ b/Scientist.Test/Comparison/ComparisonTests.cs @@ -0,0 +1,60 @@ +using FakeItEasy; +using Scientist.Test.Helpers; +using System; + +namespace Scientist.Test.Comparison +{ + public class ComparisonTests + { + [Test] + public void OverrideComparison() + { + var testMethods = A.Fake>(); + + var complexResult = A.Fake(); + complexResult.Name = "Tester"; + complexResult.Count = 10; + A.CallTo(() => testMethods.Control()).Returns(complexResult); + A.CallTo(() => testMethods.Candidate()).Returns(complexResult); + + var result = new Experiment( + name: nameof(OverrideComparison), + control: testMethods.Control, + throwOnMismatch: true + ) + .AddCandidate(testMethods.Candidate) + .Compare((a, b) => a.Count == b.Count && a.Name == b.Name) + .Run(); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + result.Should().Be(complexResult); + } + + [Test] + public void ThrowMismatchOnOverrideComparison() + { + var testMethods = A.Fake>(); + + var complexResult = A.Fake(); + complexResult.Name = "Tester"; + complexResult.Count = 10; + A.CallTo(() => testMethods.Control()).Returns(complexResult); + A.CallTo(() => testMethods.Candidate()).Returns(complexResult); + + Action act = () => new Experiment( + name: nameof(ThrowMismatchOnOverrideComparison), + control: testMethods.Control, + throwOnMismatch: true + ) + .AddCandidate(testMethods.Candidate) + .Compare((a, b) => a.Count == 2 && a.Name == b.Name) + .Run(); + + act.Should().Throw>() + .WithMessage($"Experiment '{nameof(ThrowMismatchOnOverrideComparison)}' observations mismatched"); + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + } + } +} diff --git a/Scientist.Test/Experiment/ExperimentTests.cs b/Scientist.Test/Experiment/ExperimentTests.cs new file mode 100644 index 0000000..d044da0 --- /dev/null +++ b/Scientist.Test/Experiment/ExperimentTests.cs @@ -0,0 +1,111 @@ +using FakeItEasy; +using Scientist.Test.Helpers; +using System; + +namespace Scientist.Test.Experiment +{ + public class ExperimentTests + { + [Test] + public void WithoutACandidate() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(actualResult); + + var result = new Experiment(name: nameof(WithoutACandidate), control: testMethods.Control, throwOnMismatch: true).Run(); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + result.Should().Be(actualResult); + } + + [Test] + public void WithACandidate() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(actualResult); + + var result = new Experiment(name: nameof(WithACandidate), control: testMethods.Control, throwOnMismatch: true).AddCandidate(testMethods.Candidate) + .Run(); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + result.Should().Be(actualResult); + } + + [Test] + public void WithMultipleCandidates() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + var fakeCandidate = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(actualResult); + A.CallTo(() => fakeCandidate()).Returns(actualResult); + + var result = new Experiment( + name: nameof(WithMultipleCandidates), + control: testMethods.Control, throwOnMismatch: true) + .AddCandidate(testMethods.Candidate) + .AddCandidate(fakeCandidate) + .Run(); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + A.CallTo(() => fakeCandidate()).MustHaveHappened(); + result.Should().Be(actualResult); + } + + [Test] + public void ThrowOnMismatch() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + var fakeCandidate = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(33); + A.CallTo(() => fakeCandidate()).Returns(21); + + Action act = () => new Experiment( + name: nameof(ThrowOnMismatch), + control: testMethods.Control, throwOnMismatch: true) + .AddCandidate(testMethods.Candidate) + .AddCandidate(testMethods.Candidate) + .Run(); + + act.Should().Throw>() + .WithMessage($"Experiment '{nameof(ThrowOnMismatch)}' observations mismatched"); + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + } + + + [Test] + public void ThrowOnMismatchTurnedOffDoesntThrow() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + var fakeCandidate = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(33); + + Action act = () => new Experiment( + name: nameof(ThrowOnMismatchTurnedOffDoesntThrow), + control: testMethods.Control) + .AddCandidate(testMethods.Candidate) + .Run(); + + act.Should().NotThrow>(); + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + } + } +} diff --git a/Scientist.Test/GlobalUsings.cs b/Scientist.Test/GlobalUsings.cs new file mode 100644 index 0000000..d6cca5f --- /dev/null +++ b/Scientist.Test/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using NUnit.Framework; +global using FluentAssertions; \ No newline at end of file diff --git a/Scientist.Test/Helpers/ComplexResult.cs b/Scientist.Test/Helpers/ComplexResult.cs new file mode 100644 index 0000000..12af494 --- /dev/null +++ b/Scientist.Test/Helpers/ComplexResult.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Scientist.Test.Helpers +{ + public class ComplexResult : IEquatable + { + public int Count { get; set; } + public string Name { get; set; } + + public bool Equals(ComplexResult other) + { + if (other == null) + return false; + + return Count == other.Count && Name == other.Name; + } + } +} diff --git a/Scientist.Test/Helpers/ITestClass.cs b/Scientist.Test/Helpers/ITestClass.cs new file mode 100644 index 0000000..5f8a6c1 --- /dev/null +++ b/Scientist.Test/Helpers/ITestClass.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Scientist.Test.Helpers +{ + public interface ITestClass + { + T Control(); + T Candidate(); + } +} diff --git a/Scientist.Test/Publishing/PublishingTests.cs b/Scientist.Test/Publishing/PublishingTests.cs new file mode 100644 index 0000000..5b181ab --- /dev/null +++ b/Scientist.Test/Publishing/PublishingTests.cs @@ -0,0 +1,14 @@ +namespace Scientist.Test.Publishing +{ + public class PublishingTests + { + [Test] + public void DefaultPublisher() { } + + [Test] + public void CustomPublisher() { } + + [Test] + public void FireAndForget() { } + } +} diff --git a/Scientist.Test/RunIf/RunIfTests.cs b/Scientist.Test/RunIf/RunIfTests.cs new file mode 100644 index 0000000..8ac8770 --- /dev/null +++ b/Scientist.Test/RunIf/RunIfTests.cs @@ -0,0 +1,54 @@ +using FakeItEasy; +using Scientist.Test.Helpers; +using System; + +namespace Scientist.Test.RunIf +{ + public class RunIfTests + { + + [Test] + public void RunIfFalse() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + var runIf = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => runIf()).Returns(false); + + var result = new Experiment( + name: nameof(RunIfFalse), + control: testMethods.Control) + .AddCandidate(testMethods.Candidate) + .RunIf(runIf); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => runIf()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustNotHaveHappened(); + result.Should().Be(actualResult); + } + + [Test] + public void RunIfTrue() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + var runIf = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => runIf()).Returns(true); + + var result = new Experiment( + name: nameof(RunIfTrue), + control: testMethods.Control) + .AddCandidate(testMethods.Candidate) + .RunIf(runIf); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => runIf()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + result.Should().Be(actualResult); + } + } +} diff --git a/Scientist.Test/Scientist.Test.csproj b/Scientist.Test/Scientist.Test.csproj index ad74f98..5367651 100644 --- a/Scientist.Test/Scientist.Test.csproj +++ b/Scientist.Test/Scientist.Test.csproj @@ -9,18 +9,19 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -30,6 +31,10 @@ + + + + true diff --git a/Scientist.Test/Scientist.cs b/Scientist.Test/Scientist.cs deleted file mode 100644 index 00bf97f..0000000 --- a/Scientist.Test/Scientist.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace Scientist.Test -{ - public class ScientistTest - { - [Fact] - public void Test_Method() - { - - } - } -} diff --git a/Scientist.Test/packages.lock.json b/Scientist.Test/packages.lock.json index 96be846..519f71e 100644 --- a/Scientist.Test/packages.lock.json +++ b/Scientist.Test/packages.lock.json @@ -4,288 +4,245 @@ "net8.0": { "coverlet.collector": { "type": "Direct", - "requested": "[3.1.0, )", - "resolved": "3.1.0", - "contentHash": "YzYqSRrjoP5lULBhTDcTOjuM4IDPPi6PhFsl4w8EI4WdZhE6llt7E38Tg4CHyrS+QKwyu1+9OwkdDgluOdoBTw==" + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" }, "coverlet.msbuild": { "type": "Direct", - "requested": "[3.1.0, )", - "resolved": "3.1.0", - "contentHash": "xOv5HZagq0I6uF4vt8NVVpwcA2W/uGmNcbLStao2eMk98RQH5NFPQuouh2nWWPSZrNIhGG/iYbBn2pO4s5i+ig==" + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "cUKI50VSVBqDQDFIdBnjN0Jk5JYhjtD0geP2BZZv71/ZrKORJXcLRswFsojI+cHdV+rFUL2XDJEpuhK3x1YqLA==" + }, + "FakeItEasy": { + "type": "Direct", + "requested": "[8.1.0, )", + "resolved": "8.1.0", + "contentHash": "KYNUq1hyfCE9LsMg4UGlX9zuCKd6pZNNKImjU+CaGCLU0Uf7EDr4WklOMgmsv4aGjqGtXh0WKCiklsUDCuTGqQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "FluentAssertions": { + "type": "Direct", + "requested": "[6.12.0, )", + "resolved": "6.12.0", + "contentHash": "ZXhHT2YwP9lajrwSKbLlFqsmCCvFJMoRSK9t7sImfnCyd0OB3MhgxdoMcVqxbq1iyxD6mD2fiackWmBb7ayiXQ==", + "dependencies": { + "System.Configuration.ConfigurationManager": "4.4.0" + } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Direct", - "requested": "[5.0.0, )", - "resolved": "5.0.0", - "contentHash": "+tK3seG68106lN277YWQvqmfyI/89w0uTu/5Gz5VYSUu5TI4mqwsaWLlSmT9Bl1yW/i1Nr06gHJxqaqB5NU9Tw==", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "5.0.0", - "Microsoft.Extensions.Configuration.Json": "5.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "5.0.0", - "Microsoft.Extensions.FileProviders.Physical": "5.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Json": "8.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[16.10.0, )", - "resolved": "16.10.0", - "contentHash": "/9x6TV1SUi+rtKi8UYa7ml7SEWhb0A5FuyeF0nwwUKVjdk5WaWuLPjntHVWoDuYP25KBruoxWxs7WdhDMjWxXw==", + "requested": "[17.8.0, )", + "resolved": "17.8.0", + "contentHash": "BmTYGbD/YuDHmApIENdoyN1jCk0Rj1fJB0+B/fVekyTdVidr91IlzhqzytiUgaEAzL1ZJcYCme0MeBMYvJVzvw==", "dependencies": { - "Microsoft.CodeCoverage": "16.10.0", - "Microsoft.TestPlatform.TestHost": "16.10.0" + "Microsoft.CodeCoverage": "17.8.0", + "Microsoft.TestPlatform.TestHost": "17.8.0" } }, - "xunit": { + "NUnit": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "jNTHZ01hJsDNPDSBycoHpavFZUBf9vRVQLCyuo78LRrrFj6Ol/CeqK+NIeTk5d8Eycjk59KseWb7X5Ge6z7CgQ==" + }, + "NUnit3TestAdapter": { + "type": "Direct", + "requested": "[4.5.0, )", + "resolved": "4.5.0", + "contentHash": "s8JpqTe9bI2f49Pfr3dFRfoVSuFQyraTj68c3XXjIS/MRGvvkLnrg6RLqnTjdShX+AdFUCCU/4Xex58AdUfs6A==" + }, + "System.Net.Http": { "type": "Direct", - "requested": "[2.4.1, )", - "resolved": "2.4.1", - "contentHash": "XNR3Yz9QTtec16O0aKcO6+baVNpXmOnPUxDkCY97J+8krUYxPvXT1szYYEUdKk4sB8GOI2YbAjRIOm8ZnXRfzQ==", + "requested": "[4.3.4, )", + "resolved": "4.3.4", + "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", "dependencies": { - "xunit.analyzers": "0.10.0", - "xunit.assert": "[2.4.1]", - "xunit.core": "[2.4.1]" + "Microsoft.NETCore.Platforms": "1.1.1", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" } }, - "xunit.runner.visualstudio": { + "System.Text.RegularExpressions": { "type": "Direct", - "requested": "[2.4.3, )", - "resolved": "2.4.3", - "contentHash": "kZZSmOmKA8OBlAJaquPXnJJLM9RwQ27H7BMVqfMLUcTi9xHinWGJiWksa3D4NEtz0wZ/nxd2mogObvBgJKCRhQ==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "16.10.0", - "contentHash": "7g0UjAwhEi2OBBv8SDV3wZ6J103cQyZbKVgDy59fnNdlbv0XpUCfdBZiSW5yVK/d2jp6faCdGh7VnI/F2JZO+Q==" + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==", + "dependencies": { + "System.Runtime": "4.3.1" + } }, - "Microsoft.CSharp": { + "Castle.Core": { "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "17h8b5mXa87XYKrrVqdgZ38JefSUqLChUQpXgSnpzsM0nDOhE40FTeNWOJ/YmySGV6tG6T8+hjz6vxbknHJr6A==", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Dynamic.Runtime": "4.0.11", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Linq.Expressions": "4.1.0", - "System.ObjectModel": "4.0.12", - "System.Reflection": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Threading": "4.0.11" + "System.Diagnostics.EventLog": "6.0.0" } }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "KC8SXWbGIdoFVdlxKk9WHccm0llm9HypcHMLUUFabRiTS3SO2fQXNZfdiF3qkEdTJhbRrxhdRxjL4jbtwPq4Ew==" + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "LN322qEKHjuVEhhXueTUe7RNePooZmS8aGid5aK2woX3NPjSnONFyKUc6+JknOS6ce6h2tCLfKPTBXE3mN/6Ag==", + "resolved": "8.0.0", + "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "5.0.0", - "Microsoft.Extensions.Primitives": "5.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ETjSBHMp3OAZ4HxGQYpwyGsD8Sw5FegQXphi0rpoGMT74S4+I2mm7XJEswwn59XAaKOzC15oDSOWEE8SzDCd6Q==", + "resolved": "8.0.0", + "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "dependencies": { - "Microsoft.Extensions.Primitives": "5.0.0" + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "rRdspYKA18ViPOISwAihhCMbusHsARCOtDMwa23f+BGEdIjpKPlhs3LLjmKlxfhpGXBjIsS0JpXcChjRUN+PAw==", + "resolved": "8.0.0", + "contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==", "dependencies": { - "Microsoft.Extensions.Configuration": "5.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "5.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "5.0.0", - "Microsoft.Extensions.FileProviders.Physical": "5.0.0", - "Microsoft.Extensions.Primitives": "5.0.0" + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Physical": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "Pak8ymSUfdzPfBTLHxeOwcR32YDbuVfhnH2hkfOLnJNQd19ItlBdpMjIDY9C5O/nS2Sn9bzDMai0ZrvF7KyY/Q==", + "resolved": "8.0.0", + "contentHash": "C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==", "dependencies": { - "Microsoft.Extensions.Configuration": "5.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "5.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "5.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "5.0.0" + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "System.Text.Json": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "iuZIiZ3mteEb+nsUqpGXKx2cGF+cv6gWPd5jqQI4hzqdiJ6I94ddLjKhQOuRW1lueHwocIw30xbSHGhQj0zjdQ==", + "resolved": "8.0.0", + "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", "dependencies": { - "Microsoft.Extensions.Primitives": "5.0.0" + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "1rkd8UO2qf21biwO7X0hL9uHP7vtfmdv/NLvKgCRHkdz1XnW8zVQJXyEYiN68WYpExgtVWn55QF0qBzgfh1mGg==", + "resolved": "8.0.0", + "contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "5.0.0", - "Microsoft.Extensions.FileSystemGlobbing": "5.0.0", - "Microsoft.Extensions.Primitives": "5.0.0" + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ArliS8lGk8sWRtrWpqI8yUVYJpRruPjCDT+EIjrgkA/AAPRctlAkRISVZ334chAKktTLzD1+PK8F5IZpGedSqA==" + "resolved": "8.0.0", + "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "cI/VWn9G1fghXrNDagX9nYaaB/nokkZn0HYAawGaELQrl8InSezfe9OnfPZLcJq3esXxygh3hkq2c3qoV3SDyQ==" + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + "resolved": "1.1.1", + "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + "resolved": "1.1.3", + "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "16.10.0", - "contentHash": "DYp9eKg3zffZuePhgdUrh5tHkt1YOaSraVH87r4WXDOjag1/n08aFl1vRhWP8y2RoBLTHdcZRTDOhQyYMxAYNg==", + "resolved": "17.8.0", + "contentHash": "AYy6vlpGMfz5kOFq99L93RGbqftW/8eQTqjT9iGXW6s9MRP3UdtY8idJ8rJcjeSja8A18IhIro5YnH3uv1nz4g==", "dependencies": { - "NuGet.Frameworks": "5.0.0", + "NuGet.Frameworks": "6.5.0", "System.Reflection.Metadata": "1.6.0" } }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "16.10.0", - "contentHash": "KAlB2QQRwznIH02WNl9eAuUP6/tn4IbAw4EXrvV1POTUjxuv4Dqg0u3Nn5lC9T3WIHupCHfsTcJMgsJYdi31Ig==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "16.10.0", - "Newtonsoft.Json": "9.0.1" - } - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "resolved": "17.8.0", + "contentHash": "9ivcl/7SGRmOT0YYrHQGohWiT5YCpkmy/UEzldfVisLm6QxbLaK3FAJqZXI34rnRLmqqDCeMQxKINwmKwAPiDw==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "NETStandard.Library": { - "type": "Transitive", - "resolved": "1.6.1", - "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.AppContext": "4.3.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Console": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.Compression.ZipFile": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Linq": "4.3.0", - "System.Linq.Expressions": "4.3.0", - "System.Net.Http": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Net.Sockets": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Timer": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0", - "System.Xml.XDocument": "4.3.0" + "Microsoft.TestPlatform.ObjectModel": "17.8.0", + "Newtonsoft.Json": "13.0.1" } }, "Newtonsoft.Json": { "type": "Transitive", - "resolved": "9.0.1", - "contentHash": "U82mHQSKaIk+lpSVCbWYKNavmNH1i5xrExDEquU1i6I5pV6UMOqRnJRSlKO3cMPfcpp0RgDY+8jUXHdQ4IfXvw==", - "dependencies": { - "Microsoft.CSharp": "4.0.1", - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Dynamic.Runtime": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Linq": "4.1.0", - "System.Linq.Expressions": "4.1.0", - "System.ObjectModel": "4.0.12", - "System.Reflection": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Serialization.Primitives": "4.1.1", - "System.Text.Encoding": "4.0.11", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Text.RegularExpressions": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XDocument": "4.0.11" - } + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" }, "NuGet.Frameworks": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "c5JVjuVAm4f7E9Vj+v09Z9s2ZsqFDjBpcsyS3M9xRo0bEdm/LVZSzLxxNvfvAwRiiE8nwe1h2G4OwiwlzFKXlA==" + "resolved": "6.5.0", + "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==" }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + "resolved": "4.3.2", + "contentHash": "7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==" }, "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + "resolved": "4.3.2", + "contentHash": "0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==" }, "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + "resolved": "4.3.2", + "contentHash": "G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==" }, "runtime.native.System": { "type": "Transitive", @@ -296,15 +253,6 @@ "Microsoft.NETCore.Targets": "1.1.0" } }, - "runtime.native.System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, "runtime.native.System.Net.Http": { "type": "Transitive", "resolved": "4.3.0", @@ -324,30 +272,30 @@ }, "runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "resolved": "4.3.2", + "contentHash": "QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==", "dependencies": { - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" } }, "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + "resolved": "4.3.2", + "contentHash": "I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==" }, "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + "resolved": "4.3.2", + "contentHash": "1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==" }, "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { "type": "Transitive", @@ -356,48 +304,28 @@ }, "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + "resolved": "4.3.2", + "contentHash": "6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==" }, "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + "resolved": "4.3.2", + "contentHash": "vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==" }, "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + "resolved": "4.3.2", + "contentHash": "7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==" }, "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + "resolved": "4.3.2", + "contentHash": "xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==" }, "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" - }, - "System.AppContext": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } + "resolved": "4.3.2", + "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" }, "System.Collections": { "type": "Transitive", @@ -426,16 +354,12 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Console": { + "System.Configuration.ConfigurationManager": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "resolved": "4.4.0", + "contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" + "System.Security.Cryptography.ProtectedData": "4.4.0" } }, "System.Diagnostics.Debug": { @@ -460,15 +384,10 @@ "System.Threading": "4.3.0" } }, - "System.Diagnostics.Tools": { + "System.Diagnostics.EventLog": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.Diagnostics.Tracing": { "type": "Transitive", @@ -480,28 +399,6 @@ "System.Runtime": "4.3.0" } }, - "System.Dynamic.Runtime": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "db34f6LHYM0U0JpE+sOmjar27BnqTVkbLJhgfwMpTdgTigG/Hna3m2MYVwnFzGGKnEJk2UXFuoVTr8WUbU91/A==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Linq.Expressions": "4.1.0", - "System.ObjectModel": "4.0.12", - "System.Reflection": "4.1.0", - "System.Reflection.Emit": "4.0.1", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, "System.Globalization": { "type": "Transitive", "resolved": "4.3.0", @@ -548,44 +445,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Buffers": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.IO.Compression": "4.3.0" - } - }, - "System.IO.Compression.ZipFile": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", - "dependencies": { - "System.Buffers": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, "System.IO.FileSystem": { "type": "Transitive", "resolved": "4.3.0", @@ -621,63 +480,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Linq.Expressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Linq": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Emit.Lightweight": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.DiagnosticSource": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, "System.Net.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -689,31 +491,6 @@ "System.Runtime.Handles": "4.3.0" } }, - "System.Net.Sockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.ObjectModel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, "System.Reflection": { "type": "Transitive", "resolved": "4.3.0", @@ -726,50 +503,6 @@ "System.Runtime": "4.3.0" } }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", - "dependencies": { - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit.ILGeneration": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, "System.Reflection.Metadata": { "type": "Transitive", "resolved": "1.6.0", @@ -785,15 +518,6 @@ "System.Runtime": "4.3.0" } }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, "System.Resources.ResourceManager": { "type": "Transitive", "resolved": "4.3.0", @@ -808,11 +532,11 @@ }, "System.Runtime": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "resolved": "4.3.1", + "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3" } }, "System.Runtime.Extensions": { @@ -848,20 +572,6 @@ "System.Runtime.Handles": "4.3.0" } }, - "System.Runtime.InteropServices.RuntimeInformation": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, "System.Runtime.Numerics": { "type": "Transitive", "resolved": "4.3.0", @@ -873,15 +583,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.1.1", - "contentHash": "HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==", - "dependencies": { - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0" - } - }, "System.Security.Cryptography.Algorithms": { "type": "Transitive", "resolved": "4.3.0", @@ -994,6 +695,11 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog==" + }, "System.Security.Cryptography.X509Certificates": { "type": "Transitive", "resolved": "4.3.0", @@ -1036,23 +742,17 @@ "System.Runtime": "4.3.0" } }, - "System.Text.Encoding.Extensions": { + "System.Text.Encodings.Web": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" }, - "System.Text.RegularExpressions": { + "System.Text.Json": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "resolved": "8.0.0", + "contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==", "dependencies": { - "System.Runtime": "4.3.0" + "System.Text.Encodings.Web": "8.0.0" } }, "System.Threading": { @@ -1074,112 +774,6 @@ "System.Runtime": "4.3.0" } }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Timer": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Xml.ReaderWriter": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Tasks.Extensions": "4.3.0" - } - }, - "System.Xml.XDocument": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0" - } - }, - "xunit.abstractions": { - "type": "Transitive", - "resolved": "2.0.3", - "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" - }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "0.10.0", - "contentHash": "4/IDFCJfIeg6bix9apmUtIMwvOsiwqdEexeO/R2D4GReIGPLIRODTpId/l4LRSrAJk9lEO3Zx1H0Zx6uohJDNg==" - }, - "xunit.assert": { - "type": "Transitive", - "resolved": "2.4.1", - "contentHash": "O/Oe0BS5RmSsM+LQOb041TzuPo5MdH2Rov+qXGS37X+KFG1Hxz7kopYklM5+1Y+tRGeXrOx5+Xne1RuqLFQoyQ==", - "dependencies": { - "NETStandard.Library": "1.6.1" - } - }, - "xunit.core": { - "type": "Transitive", - "resolved": "2.4.1", - "contentHash": "Zsj5OMU6JasNGERXZy8s72+pcheG6Q15atS5XpZXqAtULuyQiQ6XNnUsp1gyfC6WgqScqMvySiEHmHcOG6Eg0Q==", - "dependencies": { - "xunit.extensibility.core": "[2.4.1]", - "xunit.extensibility.execution": "[2.4.1]" - } - }, - "xunit.extensibility.core": { - "type": "Transitive", - "resolved": "2.4.1", - "contentHash": "yKZKm/8QNZnBnGZFD9SewkllHBiK0DThybQD/G4PiAmQjKtEZyHi6ET70QPU9KtSMJGRYS6Syk7EyR2EVDU4Kg==", - "dependencies": { - "NETStandard.Library": "1.6.1", - "xunit.abstractions": "2.0.3" - } - }, - "xunit.extensibility.execution": { - "type": "Transitive", - "resolved": "2.4.1", - "contentHash": "7e/1jqBpcb7frLkB6XDrHCGXAbKN4Rtdb88epYxCSRQuZDRW8UtTfdTEVpdTl8s4T56e07hOBVd4G0OdCxIY2A==", - "dependencies": { - "NETStandard.Library": "1.6.1", - "xunit.extensibility.core": "[2.4.1]" - } - }, "scientist": { "type": "Project" } From 9c5bf545dd6b039e8c0ac8ccbfc203095e23bcad Mon Sep 17 00:00:00 2001 From: Joshua Hiles Date: Mon, 8 Jan 2024 18:43:27 +0000 Subject: [PATCH 5/6] refactor: Start of simple rewrite with fluent api in mind --- Scientist/Experiment.cs | 74 ++++++++++++ Scientist/IExperiment.cs | 12 ++ Scientist/IScientist.cs | 7 -- Scientist/MismatchException.cs | 18 +++ Scientist/Scientist.cs | 198 --------------------------------- Scientist/Scientist.csproj | 24 ++++ 6 files changed, 128 insertions(+), 205 deletions(-) create mode 100644 Scientist/Experiment.cs create mode 100644 Scientist/IExperiment.cs delete mode 100644 Scientist/IScientist.cs create mode 100644 Scientist/MismatchException.cs delete mode 100644 Scientist/Scientist.cs diff --git a/Scientist/Experiment.cs b/Scientist/Experiment.cs new file mode 100644 index 0000000..2f7858a --- /dev/null +++ b/Scientist/Experiment.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; + +namespace Scientist +{ + public class Experiment(string name, Func control, bool throwOnMismatch = false) : IExperiment + { + private readonly List> _candidates = []; + public IExperiment AddCandidate(Func candidate) + { + _candidates.Add(candidate); + return this; + } + + private Func _comparison = DefaultComparison; + private bool _comparisonOverride = false; + public IExperiment Compare(Func comparison) + { + if (_comparisonOverride) + throw new ArgumentException("Compare has already been set", nameof(comparison)); + + _comparisonOverride = true; + _comparison = comparison; + + return this; + } + static readonly Func DefaultComparison = (instance, comparand) => + { + return (instance == null && comparand == null) + || (instance != null && instance.Equals(comparand)) + || (CompareInstances(instance as IEquatable, comparand)); + }; + static bool CompareInstances(IEquatable instance, T comparand) => instance != null && instance.Equals(comparand); + + + public T Run() + { + var controlResult = control.Invoke(); + + CandidateRunner(controlResult); + + return controlResult; + } + + public T RunIf(Func runIf) + { + var controlResult = control.Invoke(); + if (runIf.Invoke()) + { + CandidateRunner(controlResult); + } + else + { + // TODO: Should alert that runIf was false? + } + + return controlResult; + } + + private void CandidateRunner(T controlResult) + { + foreach (var candidate in _candidates) + { + var candidateResult = candidate.Invoke(); + + var result = _comparison(controlResult, controlResult); + if (candidateResult != null && !result && throwOnMismatch) + { + throw new MismatchException(name, candidateResult); + } + } + } + } +} diff --git a/Scientist/IExperiment.cs b/Scientist/IExperiment.cs new file mode 100644 index 0000000..4e33b8b --- /dev/null +++ b/Scientist/IExperiment.cs @@ -0,0 +1,12 @@ +using System; + +namespace Scientist +{ + public interface IExperiment + { + IExperiment AddCandidate(Func candidate); + IExperiment Compare(Func comparison); + T Run(); + T RunIf(Func func); + } +} diff --git a/Scientist/IScientist.cs b/Scientist/IScientist.cs deleted file mode 100644 index 9316d5e..0000000 --- a/Scientist/IScientist.cs +++ /dev/null @@ -1,7 +0,0 @@ - -namespace Scientist -{ - public interface IScientist - { - } -} diff --git a/Scientist/MismatchException.cs b/Scientist/MismatchException.cs new file mode 100644 index 0000000..2e9b079 --- /dev/null +++ b/Scientist/MismatchException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Scientist +{ + public class MismatchException : Exception + { + public MismatchException(string name, T result) + : base($"Experiment '{name}' observations mismatched") + { + Name = name; + Result = result; + } + + public string Name { get; } + + public T Result { get; } + } +} diff --git a/Scientist/Scientist.cs b/Scientist/Scientist.cs deleted file mode 100644 index 51131a5..0000000 --- a/Scientist/Scientist.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Scientist -{ - public class Scientist : IScientist - { - //static readonly Task EnabledTask = Task.FromResult(true); - //static readonly Lazy _sharedScientist = new Lazy(CreateSharedInstance); - //static Func> _enabled = () => EnabledTask; - //static IResultPublisher _sharedPublisher = new InMemoryResultPublisher(); - //readonly IResultPublisher _resultPublisher; - - ///// - ///// Initializes a new instance of the class - ///// using the specified - ///// - //public Scientist(IResultPublisher resultPublisher) - //{ - // _resultPublisher = resultPublisher - // ?? throw new ArgumentNullException(nameof(resultPublisher), "A result publisher must be specified"); - //} - - //// TODO: How can we guide the developer to the pit of success - - ///// - ///// Gets or sets the result publisher to use. - ///// This should be configured once before starting observations. - ///// - ///// - ///// An attempt to set the value was made after the first experiment has been run. - ///// - //public static IResultPublisher ResultPublisher - //{ - // get - // { - // return _sharedPublisher; - // } - - // set - // { - // if (_sharedScientist.IsValueCreated) - // { - // throw new InvalidOperationException($"The value of the {nameof(ResultPublisher)} property cannot be changed once an experiment has been run."); - // } - - // _sharedPublisher = value; - // } - //} - - //static Scientist CreateSharedInstance() => new SharedScientist(ResultPublisher); - - //Experiment Build(string name, int concurrentTasks, Action> experiment) - //{ - // // TODO: Maybe we could automatically generate the name if none is provided using the calling method name. We'd have to - // // make sure we don't inline this method though. - // var experimentBuilder = new Experiment(name, IsEnabledAsync, concurrentTasks, _resultPublisher); - - // experiment(experimentBuilder); - - // return experimentBuilder; - //} - - //Experiment Build(string name, int concurrentTasks, Action> experiment) - //{ - // var builder = new Experiment(name, IsEnabledAsync, concurrentTasks, _resultPublisher); - - // experiment(builder); - - // return builder; - //} - - ///// - ///// Determines if an experiment should be enabled. - ///// - ///// A delegate returning if an experiment should run. - //public static void Enabled(Func enabled) => Enabled(() => Task.FromResult(enabled())); - - ///// - ///// Determines if an experiment should be enabled. - ///// - ///// A delegate returning an asynchronous task determining if an experiment should run. - //public static void Enabled(Func> enabled) => _enabled = enabled; - - ///// - ///// Conduct a synchronous experiment - ///// - ///// The return type of the experiment - ///// Name of the experiment - ///// Experiment callback used to configure the experiment - ///// The value of the experiment's control function. - //public static T Science(string name, Action> experiment) => - // _sharedScientist.Value.Experiment(name, experiment); - - ///// - ///// Conduct a synchronous experiment - ///// - ///// The return type of the experiment - ///// The clean type for publishing. - ///// Name of the experiment - ///// Experiment callback used to configure the experiment - ///// The value of the experiment's control function. - //public static T Science(string name, Action> experiment) => - // _sharedScientist.Value.Experiment(name, experiment); - - ///// - ///// Conduct an asynchronous experiment - ///// - ///// The return type of the experiment - ///// Name of the experiment - ///// Experiment callback used to configure the experiment - ///// The value of the experiment's control function. - //public static Task ScienceAsync(string name, Action> experiment) => - // _sharedScientist.Value.ExperimentAsync(name, experiment); - - ///// - ///// Conduct an asynchronous experiment - ///// - ///// The return type of the experiment - ///// Name of the experiment - ///// Number of tasks to run concurrently - ///// Experiment callback used to configure the experiment - ///// The value of the experiment's control function. - //public static Task ScienceAsync(string name, int concurrentTasks, Action> experiment) => - // _sharedScientist.Value.ExperimentAsync(name, concurrentTasks, experiment); - - ///// - ///// Conduct an asynchronous experiment - ///// - ///// The return type of the experiment - ///// The clean type for publishing. - ///// Name of the experiment - ///// Experiment callback used to configure the experiment - ///// The value of the experiment's control function. - //public static Task ScienceAsync(string name, Action> experiment) => - // _sharedScientist.Value.ExperimentAsync(name, experiment); - - ///// - ///// Conduct an asynchronous experiment - ///// - ///// The return type of the experiment - ///// The clean type for publishing. - ///// Name of the experiment - ///// Number of tasks to run concurrently - ///// Experiment callback used to configure the experiment - ///// The value of the experiment's control function. - //public static Task ScienceAsync(string name, int concurrentTasks, Action> experiment) => - // _sharedScientist.Value.ExperimentAsync(name, concurrentTasks, experiment); - - ///// - ///// Conduct a synchronous experiment - ///// - ///// The return type of the experiment - ///// The clean type for publishing. - ///// Name of the experiment - ///// Experiment callback used to configure the experiment - ///// The value of the experiment's control function. - //public T Experiment(string name, Action> experiment) => - // Build(name, 1, experiment).Build().Run().Result; - - ///// - ///// Conduct an asynchronous experiment - ///// - ///// The return type of the experiment - ///// The clean type for publishing. - ///// Name of the experiment - ///// Number of tasks to run concurrently - ///// Experiment callback used to configure the experiment - ///// The value of the experiment's control function. - //public Task ExperimentAsync(string name, int concurrentTasks, Action> experiment) => - // Build(name, concurrentTasks, experiment).Build().Run(); - - ///// - ///// Returns whether the experiment is enabled as an asynchronous operation - ///// - ///// - ///// A which returns whether the experiment is enabled. - ///// - ///// - ///// Override this method to change the default implementation which always returns . - ///// - //protected virtual Task IsEnabledAsync() => EnabledTask; - - //// This class acts as a proxy to allow the static methods to set the state on an instance of Scientist. - //private sealed class SharedScientist : Scientist - //{ - // internal SharedScientist(IResultPublisher resultPublisher) - // : base(resultPublisher) - // { - // } - - // protected override async Task IsEnabledAsync() - // { - // return await _enabled().ConfigureAwait(false); - // } - //} - } -} diff --git a/Scientist/Scientist.csproj b/Scientist/Scientist.csproj index f87a350..21bd678 100644 --- a/Scientist/Scientist.csproj +++ b/Scientist/Scientist.csproj @@ -5,6 +5,30 @@ enable + + + + Scientist + 3.0.0-alpha + + + + Implementation of the Scientist library for .NET, inspired by the Ruby library from + GitHub + https://github.com/scientistproject/Scientist.net + + https://raw.githubusercontent.com/scientistproject/Scientist.net/master/assets/scientist-logo-64x64.png + + https://github.com/scientistproject/Scientist.net.git + git + scientist;refactoring + + + + MIT + https://github.com/scientistproject/Scientist.net/blob/master/LICENSE.txt + + true From b299dc652112c1ad9354f22eac82da270c982a16 Mon Sep 17 00:00:00 2001 From: Joshua Hiles Date: Fri, 19 Jan 2024 18:11:29 +0000 Subject: [PATCH 6/6] further updates with async & publishing --- Scientist.Benchmark/ExperimentBenchmarks.cs | 23 ++- Scientist.Benchmark/Program.cs | 13 +- Scientist.Test/AsyncExperimentTests.cs | 29 ++++ .../{Comparison => }/ComparisonTests.cs | 14 +- Scientist.Test/ExceptionTests.cs | 54 ++++++ .../{Experiment => }/ExperimentTests.cs | 63 ++----- Scientist.Test/Helpers/ITestClass.cs | 8 +- Scientist.Test/Helpers/ITestClassAsync.cs | 10 ++ Scientist.Test/Helpers/TestHelpers.cs | 13 ++ Scientist.Test/Publishing/PublishingTests.cs | 14 -- Scientist.Test/PublishingTests.cs | 137 ++++++++++++++++ Scientist.Test/{RunIf => }/RunIfTests.cs | 17 +- Scientist.Test/Scientist.Test.csproj | 4 - Scientist/ConcurrentSet.cs | 38 +++++ Scientist/Experiment.cs | 155 ++++++++++++++---- Scientist/FireAndForgetResultsPublisher.cs | 54 ++++++ Scientist/IExperiment.cs | 12 -- Scientist/IPublisher.cs | 13 ++ Scientist/Result.cs | 14 ++ Scientist/Results.cs | 20 +++ Scientist/Test.cs | 38 +++++ 21 files changed, 598 insertions(+), 145 deletions(-) create mode 100644 Scientist.Test/AsyncExperimentTests.cs rename Scientist.Test/{Comparison => }/ComparisonTests.cs (84%) create mode 100644 Scientist.Test/ExceptionTests.cs rename Scientist.Test/{Experiment => }/ExperimentTests.cs (51%) create mode 100644 Scientist.Test/Helpers/ITestClassAsync.cs create mode 100644 Scientist.Test/Helpers/TestHelpers.cs delete mode 100644 Scientist.Test/Publishing/PublishingTests.cs create mode 100644 Scientist.Test/PublishingTests.cs rename Scientist.Test/{RunIf => }/RunIfTests.cs (80%) create mode 100644 Scientist/ConcurrentSet.cs create mode 100644 Scientist/FireAndForgetResultsPublisher.cs delete mode 100644 Scientist/IExperiment.cs create mode 100644 Scientist/IPublisher.cs create mode 100644 Scientist/Result.cs create mode 100644 Scientist/Results.cs create mode 100644 Scientist/Test.cs diff --git a/Scientist.Benchmark/ExperimentBenchmarks.cs b/Scientist.Benchmark/ExperimentBenchmarks.cs index 20952f6..fdcf641 100644 --- a/Scientist.Benchmark/ExperimentBenchmarks.cs +++ b/Scientist.Benchmark/ExperimentBenchmarks.cs @@ -2,7 +2,7 @@ namespace Scientist.Benchmark { - [MemoryDiagnoser] + [HtmlExporter] public class ExperimentBenchmarks { readonly Experiment experiment; @@ -12,16 +12,27 @@ public ExperimentBenchmarks() experiment = new Experiment(name: "Instance", control: FortyTwo); } - [Benchmark] + [Benchmark(Baseline = true)] public int Static() { - return new Experiment(name: "Static", control: FortyTwo).Run(); + return new Experiment(nameof(Static), control: FortyTwo) + .AddCandidate(FortyTwo) + .Run(); } - [Benchmark(Baseline = false)] - public int Instance() + // Need to name candidates something + //[Benchmark] + //public int Instance() + //{ + // return experiment.AddCandidate(FortyTwo).Run(); + //} + + [Benchmark] + public int Multiple() { - return experiment.Run(); + return new Experiment(nameof(Multiple), control: FortyTwo) + .AddCandidate(FortyTwo) + .Run(); } private static int FortyTwo() => 42; diff --git a/Scientist.Benchmark/Program.cs b/Scientist.Benchmark/Program.cs index b356bdc..00cfdfe 100644 --- a/Scientist.Benchmark/Program.cs +++ b/Scientist.Benchmark/Program.cs @@ -1,6 +1,17 @@ // See https://aka.ms/new-console-template for more information +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using BenchmarkDotNet.Toolchains.InProcess.NoEmit; using Scientist.Benchmark; -var summary = BenchmarkRunner.Run(); +var config = DefaultConfig.Instance + .AddJob(Job + .MediumRun + .WithLaunchCount(1) + .WithToolchain(InProcessNoEmitToolchain.Instance)); + +var summary = BenchmarkRunner.Run(config); diff --git a/Scientist.Test/AsyncExperimentTests.cs b/Scientist.Test/AsyncExperimentTests.cs new file mode 100644 index 0000000..0e9caae --- /dev/null +++ b/Scientist.Test/AsyncExperimentTests.cs @@ -0,0 +1,29 @@ +using FakeItEasy; +using Scientist.Test.Helpers; +using System.Threading.Tasks; + +namespace Scientist.Test +{ + public class AsyncExperiments + { + [Test] + public async Task AsynchronousExperimentAsync() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(Task.FromResult(actualResult)); + A.CallTo(() => testMethods.Candidate()).Returns(Task.FromResult(actualResult)); + + var result = await new Experiment( + name: nameof(AsynchronousExperimentAsync), + control: testMethods.Control) + .AddCandidate(testMethods.Candidate) + .ThrowOnMismatch() + .RunAsync(); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + result.Should().Be(actualResult); + } + } +} diff --git a/Scientist.Test/Comparison/ComparisonTests.cs b/Scientist.Test/ComparisonTests.cs similarity index 84% rename from Scientist.Test/Comparison/ComparisonTests.cs rename to Scientist.Test/ComparisonTests.cs index 4740825..4dd4de0 100644 --- a/Scientist.Test/Comparison/ComparisonTests.cs +++ b/Scientist.Test/ComparisonTests.cs @@ -2,9 +2,9 @@ using Scientist.Test.Helpers; using System; -namespace Scientist.Test.Comparison +namespace Scientist.Test { - public class ComparisonTests + public class Comparisons { [Test] public void OverrideComparison() @@ -19,9 +19,9 @@ public void OverrideComparison() var result = new Experiment( name: nameof(OverrideComparison), - control: testMethods.Control, - throwOnMismatch: true + control: testMethods.Control ) + .ThrowOnMismatch() .AddCandidate(testMethods.Candidate) .Compare((a, b) => a.Count == b.Count && a.Name == b.Name) .Run(); @@ -44,15 +44,15 @@ public void ThrowMismatchOnOverrideComparison() Action act = () => new Experiment( name: nameof(ThrowMismatchOnOverrideComparison), - control: testMethods.Control, - throwOnMismatch: true + control: testMethods.Control ) + .ThrowOnMismatch() .AddCandidate(testMethods.Candidate) .Compare((a, b) => a.Count == 2 && a.Name == b.Name) .Run(); act.Should().Throw>() - .WithMessage($"Experiment '{nameof(ThrowMismatchOnOverrideComparison)}' observations mismatched"); + .WithMessage($"Experiment '{nameof(testMethods.Candidate)}' observations mismatched"); A.CallTo(() => testMethods.Control()).MustHaveHappened(); A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); } diff --git a/Scientist.Test/ExceptionTests.cs b/Scientist.Test/ExceptionTests.cs new file mode 100644 index 0000000..374f6c7 --- /dev/null +++ b/Scientist.Test/ExceptionTests.cs @@ -0,0 +1,54 @@ +using FakeItEasy; +using Scientist.Test.Helpers; +using System; + +namespace Scientist.Test +{ + public class Exceptions + { + + [Test] + public void ThrowOnMismatch() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(33); + + Action act = () => new Experiment( + name: nameof(ThrowOnMismatch), + control: testMethods.Control) + .ThrowOnMismatch() + .AddCandidate(testMethods.Candidate) + .Run(); + + act.Should().Throw>() + .WithMessage($"Experiment '{nameof(testMethods.Candidate)}' observations mismatched"); + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + } + + + [Test] + public void ThrowOnMismatchTurnedOffDoesntThrow() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + var fakeCandidate = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(33); + + Action act = () => new Experiment( + name: nameof(ThrowOnMismatchTurnedOffDoesntThrow), + control: testMethods.Control) + .AddCandidate(testMethods.Candidate) + .Run(); + + act.Should().NotThrow>(); + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); + } + } +} diff --git a/Scientist.Test/Experiment/ExperimentTests.cs b/Scientist.Test/ExperimentTests.cs similarity index 51% rename from Scientist.Test/Experiment/ExperimentTests.cs rename to Scientist.Test/ExperimentTests.cs index d044da0..dedc6ce 100644 --- a/Scientist.Test/Experiment/ExperimentTests.cs +++ b/Scientist.Test/ExperimentTests.cs @@ -2,9 +2,9 @@ using Scientist.Test.Helpers; using System; -namespace Scientist.Test.Experiment +namespace Scientist.Test { - public class ExperimentTests + public class Experiments { [Test] public void WithoutACandidate() @@ -15,7 +15,9 @@ public void WithoutACandidate() A.CallTo(() => testMethods.Control()).Returns(actualResult); A.CallTo(() => testMethods.Candidate()).Returns(actualResult); - var result = new Experiment(name: nameof(WithoutACandidate), control: testMethods.Control, throwOnMismatch: true).Run(); + var result = new Experiment(name: nameof(WithoutACandidate), control: testMethods.Control) + .ThrowOnMismatch() + .Run(); A.CallTo(() => testMethods.Control()).MustHaveHappened(); result.Should().Be(actualResult); @@ -30,7 +32,9 @@ public void WithACandidate() A.CallTo(() => testMethods.Control()).Returns(actualResult); A.CallTo(() => testMethods.Candidate()).Returns(actualResult); - var result = new Experiment(name: nameof(WithACandidate), control: testMethods.Control, throwOnMismatch: true).AddCandidate(testMethods.Candidate) + var result = new Experiment(name: nameof(WithACandidate), control: testMethods.Control) + .ThrowOnMismatch() + .AddCandidate(testMethods.Candidate) .Run(); A.CallTo(() => testMethods.Control()).MustHaveHappened(); @@ -51,9 +55,10 @@ public void WithMultipleCandidates() var result = new Experiment( name: nameof(WithMultipleCandidates), - control: testMethods.Control, throwOnMismatch: true) + control: testMethods.Control) + .ThrowOnMismatch() .AddCandidate(testMethods.Candidate) - .AddCandidate(fakeCandidate) + .AddCandidate(name: "Candidate 2", candidate: fakeCandidate) .Run(); A.CallTo(() => testMethods.Control()).MustHaveHappened(); @@ -61,51 +66,5 @@ public void WithMultipleCandidates() A.CallTo(() => fakeCandidate()).MustHaveHappened(); result.Should().Be(actualResult); } - - [Test] - public void ThrowOnMismatch() - { - int actualResult = 42; - - var testMethods = A.Fake>(); - var fakeCandidate = A.Fake>(); - A.CallTo(() => testMethods.Control()).Returns(actualResult); - A.CallTo(() => testMethods.Candidate()).Returns(33); - A.CallTo(() => fakeCandidate()).Returns(21); - - Action act = () => new Experiment( - name: nameof(ThrowOnMismatch), - control: testMethods.Control, throwOnMismatch: true) - .AddCandidate(testMethods.Candidate) - .AddCandidate(testMethods.Candidate) - .Run(); - - act.Should().Throw>() - .WithMessage($"Experiment '{nameof(ThrowOnMismatch)}' observations mismatched"); - A.CallTo(() => testMethods.Control()).MustHaveHappened(); - A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); - } - - - [Test] - public void ThrowOnMismatchTurnedOffDoesntThrow() - { - int actualResult = 42; - - var testMethods = A.Fake>(); - var fakeCandidate = A.Fake>(); - A.CallTo(() => testMethods.Control()).Returns(actualResult); - A.CallTo(() => testMethods.Candidate()).Returns(33); - - Action act = () => new Experiment( - name: nameof(ThrowOnMismatchTurnedOffDoesntThrow), - control: testMethods.Control) - .AddCandidate(testMethods.Candidate) - .Run(); - - act.Should().NotThrow>(); - A.CallTo(() => testMethods.Control()).MustHaveHappened(); - A.CallTo(() => testMethods.Candidate()).MustHaveHappened(); - } } } diff --git a/Scientist.Test/Helpers/ITestClass.cs b/Scientist.Test/Helpers/ITestClass.cs index 5f8a6c1..b43873b 100644 --- a/Scientist.Test/Helpers/ITestClass.cs +++ b/Scientist.Test/Helpers/ITestClass.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Scientist.Test.Helpers +namespace Scientist.Test.Helpers { public interface ITestClass { diff --git a/Scientist.Test/Helpers/ITestClassAsync.cs b/Scientist.Test/Helpers/ITestClassAsync.cs new file mode 100644 index 0000000..8f96e07 --- /dev/null +++ b/Scientist.Test/Helpers/ITestClassAsync.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Scientist.Test.Helpers +{ + public interface ITestClassAsync + { + Task Control(); + Task Candidate(); + } +} diff --git a/Scientist.Test/Helpers/TestHelpers.cs b/Scientist.Test/Helpers/TestHelpers.cs new file mode 100644 index 0000000..7c55b91 --- /dev/null +++ b/Scientist.Test/Helpers/TestHelpers.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Scientist.Test.Helpers +{ + public static class TestHelpers + { + public static IEnumerable> Results(/*string experimentName) => ResultPublisher.Results(experimentName);*/ + } +} diff --git a/Scientist.Test/Publishing/PublishingTests.cs b/Scientist.Test/Publishing/PublishingTests.cs deleted file mode 100644 index 5b181ab..0000000 --- a/Scientist.Test/Publishing/PublishingTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Scientist.Test.Publishing -{ - public class PublishingTests - { - [Test] - public void DefaultPublisher() { } - - [Test] - public void CustomPublisher() { } - - [Test] - public void FireAndForget() { } - } -} diff --git a/Scientist.Test/PublishingTests.cs b/Scientist.Test/PublishingTests.cs new file mode 100644 index 0000000..aa89238 --- /dev/null +++ b/Scientist.Test/PublishingTests.cs @@ -0,0 +1,137 @@ +using FakeItEasy; +using NUnit.Framework.Internal; +using Scientist.Test.Helpers; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Scientist.Test +{ + public class Publishing + { + [Test] + public void WithPublisher() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(actualResult); + + var testPublisher = A.Fake(); + A.CallTo(() => testPublisher.Publish(A>.Ignored)); + + var experiment = new Experiment(name: nameof(WithPublisher), control: testMethods.Control) + .ThrowOnMismatch(); + var result = experiment.Run(); + experiment.Publish(testPublisher); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + result.Should().Be(actualResult); + A.CallTo(() => testPublisher.Publish(A>.Ignored)).MustHaveHappened(); + } + + [Test] + public void WithAsyncPublisher() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(actualResult); + + var testPublisher = A.Fake(); + A.CallTo(() => testPublisher.Publish(A>.Ignored)); + + var experiment = new Experiment(name: nameof(WithAsyncPublisher), control: testMethods.Control) + .ThrowOnMismatch(); + var result = experiment.Run(); + experiment.PublishAsync(testPublisher); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + result.Should().Be(actualResult); + A.CallTo(() => testPublisher.Publish(A>.Ignored)).MustHaveHappened(); + } + + [Test] + public async Task FireAndForgetAsync() + { + int actualResult = 42; + + var pendingPublishTask = new TaskCompletionSource(); + + var testPublisher = A.Fake(); + A.CallTo(() => testPublisher.Publish(A>.Ignored)).Returns(pendingPublishTask.Task); + + var testMethods = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(actualResult); + + var fireAndForgetPublisher = new FireAndForgetResultPublisher(testPublisher, ex => { }); + + + const int count = 10; + Parallel.ForEach( + Enumerable.Repeat(0, count), + src => + { + var experiment = new Experiment( + name: $"{nameof(WithAsyncPublisher)}-{count}", + control: testMethods.Control + ) + .ThrowOnMismatch() + .AddCandidate(testMethods.Candidate); + + var result = experiment.Run(); + experiment.Publish(fireAndForgetPublisher); + + result.Should().Be(actualResult); + }); + + Task whenPublished = fireAndForgetPublisher.WhenPublished(); + whenPublished.Should().NotBeNull(); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(count, Times.Exactly); + A.CallTo(() => testMethods.Candidate()).MustHaveHappened(count, Times.Exactly); + + whenPublished.IsCompleted.Should().BeFalse("When Published Task completed early."); + + pendingPublishTask.SetResult(null); + + await whenPublished; + whenPublished.IsCompleted.Should().BeTrue("When Published Task isn't complete."); + } + + [Test] + public void WithContext() + { + int actualResult = 42; + + var testMethods = A.Fake>(); + A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(actualResult); + + var testPublisher = A.Fake(); + A.CallTo(() => testPublisher.Publish(A>.Ignored)); + + var experiment = new Experiment(name: nameof(WithPublisher), control: testMethods.Control) + .ThrowOnMismatch(); + var result = experiment.Run(); + experiment.Publish(testPublisher); + + A.CallTo(() => testMethods.Control()).MustHaveHappened(); + result.Should().Be(actualResult); + A.CallTo(() => testPublisher.Publish(A>.Ignored)).MustHaveHappened(); + + + var publishResults = TestHelper.Results(experimentName).First(); + + Assert.Equal(42, result); + Assert.Equal(1, publishResults.Contexts.Count); + + var context = publishResults.Contexts.First(); + Assert.Equal("test", context.Key); + Assert.Equal("data", context.Value); + } + } +} diff --git a/Scientist.Test/RunIf/RunIfTests.cs b/Scientist.Test/RunIfTests.cs similarity index 80% rename from Scientist.Test/RunIf/RunIfTests.cs rename to Scientist.Test/RunIfTests.cs index 8ac8770..0645ff2 100644 --- a/Scientist.Test/RunIf/RunIfTests.cs +++ b/Scientist.Test/RunIfTests.cs @@ -2,13 +2,13 @@ using Scientist.Test.Helpers; using System; -namespace Scientist.Test.RunIf +namespace Scientist.Test { - public class RunIfTests + public class RunIf { - + [Test] - public void RunIfFalse() + public void ItDoesntRunCandidate() { int actualResult = 42; @@ -18,8 +18,9 @@ public void RunIfFalse() A.CallTo(() => runIf()).Returns(false); var result = new Experiment( - name: nameof(RunIfFalse), + name: nameof(ItDoesntRunCandidate), control: testMethods.Control) + .ThrowOnMismatch() .AddCandidate(testMethods.Candidate) .RunIf(runIf); @@ -30,18 +31,20 @@ public void RunIfFalse() } [Test] - public void RunIfTrue() + public void ItRunsCandidate() { int actualResult = 42; var testMethods = A.Fake>(); var runIf = A.Fake>(); A.CallTo(() => testMethods.Control()).Returns(actualResult); + A.CallTo(() => testMethods.Candidate()).Returns(actualResult); A.CallTo(() => runIf()).Returns(true); var result = new Experiment( - name: nameof(RunIfTrue), + name: nameof(ItRunsCandidate), control: testMethods.Control) + .ThrowOnMismatch() .AddCandidate(testMethods.Candidate) .RunIf(runIf); diff --git a/Scientist.Test/Scientist.Test.csproj b/Scientist.Test/Scientist.Test.csproj index 5367651..927b659 100644 --- a/Scientist.Test/Scientist.Test.csproj +++ b/Scientist.Test/Scientist.Test.csproj @@ -31,10 +31,6 @@ - - - - true diff --git a/Scientist/ConcurrentSet.cs b/Scientist/ConcurrentSet.cs new file mode 100644 index 0000000..5c2ae3b --- /dev/null +++ b/Scientist/ConcurrentSet.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Scientist +{ + internal class ConcurrentSet + { + static readonly byte One = 1; + static readonly byte Zero = 0; + + ConcurrentDictionary _dictionary = new ConcurrentDictionary(); + + public bool IsEmpty => _dictionary.IsEmpty; + public ICollection Items => _dictionary.Keys; + + public bool ContainsKey(T item) => _dictionary.ContainsKey(item); + + public bool TryAdd(T item) + { + byte data = _dictionary.AddOrUpdate( + item, + Zero, + (key, value) => (byte)(value + One)); + + return data == Zero; + } + + public bool TryRemove(T item) + { + byte _; + return _dictionary.TryRemove(item, out _); + } + } +} diff --git a/Scientist/Experiment.cs b/Scientist/Experiment.cs index 2f7858a..4b6a4ec 100644 --- a/Scientist/Experiment.cs +++ b/Scientist/Experiment.cs @@ -1,74 +1,159 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Scientist { - public class Experiment(string name, Func control, bool throwOnMismatch = false) : IExperiment + public class Experiment { - private readonly List> _candidates = []; - public IExperiment AddCandidate(Func candidate) + public string Name = nameof(T); + private Dictionary>> _candidates = []; + private bool _asyncExperiment = false; + + public Experiment(string name, Func control) + { + Name = name; + _candidates.Add("control", () => Task.FromResult(control())); + } + public Experiment(string name, Func> control) + { + Name = name; + _asyncExperiment = true; + _candidates.Add("control", control); + } + + private bool _throwOnMismatch = false; + public Experiment ThrowOnMismatch() { - _candidates.Add(candidate); + _throwOnMismatch = true; return this; } - private Func _comparison = DefaultComparison; - private bool _comparisonOverride = false; - public IExperiment Compare(Func comparison) + public Experiment AddCandidate(Func candidate, string name = "") { - if (_comparisonOverride) - throw new ArgumentException("Compare has already been set", nameof(comparison)); + AddCandidate( + name == "" ? nameof(candidate) : name, + () => Task.FromResult(candidate()) + ); - _comparisonOverride = true; - _comparison = comparison; + return this; + } + + public Experiment AddCandidate(Func> candidate, string name = "") + { + AddCandidate(name == "" ? nameof(candidate) : name, candidate); return this; } - static readonly Func DefaultComparison = (instance, comparand) => + + private Experiment AddCandidate(string name, Func> candidate + ) + { + if (_candidates.ContainsKey(name)) + { + throw new InvalidOperationException($"You've already added {name}"); + } + _candidates.Add(name, candidate); + return this; + } + + private Func _comparator = (instance, comparand) => { return (instance == null && comparand == null) || (instance != null && instance.Equals(comparand)) - || (CompareInstances(instance as IEquatable, comparand)); + || CompareInstances(instance as IEquatable, comparand); }; - static bool CompareInstances(IEquatable instance, T comparand) => instance != null && instance.Equals(comparand); + static bool CompareInstances(IEquatable instance, T comparand) => instance != null && instance.Equals(comparand); - public T Run() + private bool _comparisonOverride = false; + public Experiment Compare(Func comparison) { - var controlResult = control.Invoke(); + if (_comparisonOverride) + throw new ArgumentException("Compare has already been set", nameof(comparison)); - CandidateRunner(controlResult); + _comparisonOverride = true; + _comparator = comparison; - return controlResult; + return this; } + private readonly Dictionary _contexts = []; + public Experiment AddContext(string key, dynamic data) + { + _contexts.Add(key, data); + return this; + } + + + private Func? _runIf; public T RunIf(Func runIf) { - var controlResult = control.Invoke(); - if (runIf.Invoke()) - { - CandidateRunner(controlResult); - } - else - { - // TODO: Should alert that runIf was false? - } + _runIf = runIf; + return Run(); + } + + private Results? _results; + public T Run() + { + return Task.Run(CandidateRunner).Result; + } - return controlResult; + public async Task RunAsync() + { + return await CandidateRunner(); } - private void CandidateRunner(T controlResult) + private async Task CandidateRunner() { - foreach (var candidate in _candidates) + var stopwatch = Stopwatch.StartNew(); + + var observations = new ConcurrentBag> { - var candidateResult = candidate.Invoke(); + await Test.Perform(control: _candidates.First(candidate => candidate.Key == "control").Value) + }; + + - var result = _comparison(controlResult, controlResult); - if (candidateResult != null && !result && throwOnMismatch) + await Parallel.ForEachAsync(_candidates.Where(candidate => candidate.Key != "control"), async(candidate, cancellationToken)=> + { + if (_runIf == null || _runIf.Invoke()) { - throw new MismatchException(name, candidateResult); + observations.Add( + await Test.Perform( + candidateName: candidate.Key, + candidate: candidate.Value, + comparator: _comparator, + controlResult: observations.First(x => x.IsControl).Value, + throwOnMismatch: _throwOnMismatch + ) + ); } - } + }); + stopwatch.Stop(); + + _results = new Results( + experimentName: Name, + overrallDuration: stopwatch.Elapsed, + results: observations, + overallMatch: observations.Any(x => x.Mismatched == true) + ); ; + + return _results.Control.Value; + } + + public void Publish(IPublisher publisher) + { + publisher.Publish(_results); + } + + public async void PublishAsync(IPublisher publisher) + { + await publisher.Publish(_results); } } } diff --git a/Scientist/FireAndForgetResultsPublisher.cs b/Scientist/FireAndForgetResultsPublisher.cs new file mode 100644 index 0000000..c8ff88a --- /dev/null +++ b/Scientist/FireAndForgetResultsPublisher.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Scientist +{ + public class FireAndForgetResultPublisher : IPublisher + { + private readonly IPublisher _publisher; + private readonly Action _onPublisherException; + private readonly ConcurrentSet _publishingTasks = new ConcurrentSet(); + + public FireAndForgetResultPublisher(IPublisher publisher) : this(publisher, e => { }) + { + } + + public FireAndForgetResultPublisher(IPublisher publisher, Action onPublisherException) + { + _publisher = publisher; + _onPublisherException = onPublisherException; + } + + public Task Publish(Results result) + { + + // Disable the warning, because the task is being tracked on the set, + // and immediately removed upon completion. +#pragma warning disable CS4014 + var subTask = Task.Run(async () => + { + try + { + await _publisher.Publish(result); + } + catch (Exception ex) + { + _onPublisherException(ex); + } + }); + _publishingTasks.TryAdd(subTask); + subTask.ContinueWith(_ => + { + _publishingTasks.TryRemove(subTask); + }); +#pragma warning restore CS4014 + return Task.FromResult((object)null); + } + + public Task WhenPublished() => + Task.WhenAll(_publishingTasks.Items); + } +} diff --git a/Scientist/IExperiment.cs b/Scientist/IExperiment.cs deleted file mode 100644 index 4e33b8b..0000000 --- a/Scientist/IExperiment.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Scientist -{ - public interface IExperiment - { - IExperiment AddCandidate(Func candidate); - IExperiment Compare(Func comparison); - T Run(); - T RunIf(Func func); - } -} diff --git a/Scientist/IPublisher.cs b/Scientist/IPublisher.cs new file mode 100644 index 0000000..07883fc --- /dev/null +++ b/Scientist/IPublisher.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Scientist +{ + public interface IPublisher + { + Task Publish(Results results); + } +} diff --git a/Scientist/Result.cs b/Scientist/Result.cs new file mode 100644 index 0000000..401af92 --- /dev/null +++ b/Scientist/Result.cs @@ -0,0 +1,14 @@ + +using System; + +namespace Scientist +{ + public class Result(string name, TimeSpan duration, T value, bool isControl = false) + { + public string Name = name; + public TimeSpan Duration = duration; + public bool? Mismatched; + public bool IsControl = isControl; + public T Value = value; + } +} diff --git a/Scientist/Results.cs b/Scientist/Results.cs new file mode 100644 index 0000000..83bc224 --- /dev/null +++ b/Scientist/Results.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; + +namespace Scientist +{ + public class Results( + string experimentName, + TimeSpan overrallDuration, + ConcurrentBag> results, + bool overallMatch + ) + { + public string ExperimentName = experimentName; + public TimeSpan OverrallDuration = overrallDuration; + public bool OverrallMatch = overallMatch; + public Result Control = results.First(result => result.IsControl); + public ConcurrentBag> Candidates = new(results.Where(x => !x.IsControl)); + } +} diff --git a/Scientist/Test.cs b/Scientist/Test.cs new file mode 100644 index 0000000..8bc2d81 --- /dev/null +++ b/Scientist/Test.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Scientist +{ + internal static class Test + { + public static async Task> Perform(Func> control) + { + return await Run("Control", control); + } + public static async Task> Perform(string candidateName, Func> candidate, Func comparator, T controlResult, bool throwOnMismatch) + { + var result = await Run(candidateName, candidate); + var valuesAreEqual = comparator(controlResult, result.Value); + if (result.Value != null && !valuesAreEqual) + { + result.Mismatched = true; + + if (throwOnMismatch) + throw new MismatchException(nameof(candidate), result.Value); + } + result.Mismatched = false; + + return result; + } + + private static async Task> Run(string name, Func> function) + { + var stopwatch = Stopwatch.StartNew(); + var result = await Task.Run(function); + stopwatch.Stop(); + + return new Result(name: name, duration: stopwatch.Elapsed, value: result, isControl: true); + } + } +}