Skip to content

Commit

Permalink
add some functional extension tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dit-zy committed Feb 5, 2024
1 parent 1f2fcbb commit 828d098
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 51 deletions.
20 changes: 20 additions & 0 deletions ScoutHelper/Utils/CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ public static IDictionary<K, V> ToDict<K, V>(this IEnumerable<(K, V)> source) wh
.DistinctBy(entry => entry.Item1)
.ToImmutableDictionary(entry => entry.Item1, entry => entry.Item2);

public static IDictionary<K, V> ToMutableDict<K, V>(this IEnumerable<KeyValuePair<K, V>> source) where K : notnull =>
source.Select(entry => (entry.Key, entry.Value)).ToMutableDict();

public static IDictionary<K, V> ToMutableDict<K, V>(this IEnumerable<(K, V)> source) where K : notnull =>
source
.DistinctBy(entry => entry.Item1)
.ToDictionary(entry => entry.Item1, entry => entry.Item2);

public static IDictionary<K, V> With<K, V>(this IDictionary<K, V> source, params (K, V)[] entries) where K : notnull {
var dict = source.IsReadOnly ? source.ToMutableDict() : source;
entries.ForEach(entry => dict[entry.Item1] = entry.Item2);
return source.IsReadOnly ? dict.ToDict() : source;
}

public static IDictionary<K, V> Without<K, V>(this IDictionary<K, V> source, params K[] keys) where K : notnull {
var dict = source.IsReadOnly ? source.ToMutableDict() : source;
keys.ForEach(key => dict.Remove(key));
return source.IsReadOnly ? dict.ToDict() : source;
}

public static IEnumerable<U?> SelectWhere<T, U>(this IEnumerable<T> source, Func<T, (bool, U?)> filteredSelector) =>
source
.Select(filteredSelector)
Expand Down
6 changes: 4 additions & 2 deletions ScoutHelper/Utils/Functional/FunctionalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ namespace ScoutHelper.Utils.Functional;
public static class FunctionalExtensions {
#region maybe

public static IEnumerable<U> SelectMaybe<T, U>(this IEnumerable<T> source, Func<T, Maybe<U>> selector)
where U : struct =>
public static IEnumerable<T> SelectMaybe<T>(this IEnumerable<Maybe<T>> source) =>
source.SelectMaybe(value => value);

public static IEnumerable<U> SelectMaybe<T, U>(this IEnumerable<T> source, Func<T, Maybe<U>> selector) =>
source.SelectWhere(
value => {
var result = selector.Invoke(value);
Expand Down
23 changes: 14 additions & 9 deletions ScoutHelper/Utils/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
using CSharpFunctionalExtensions;
using ImGuiNET;
using ScoutHelper.Models;

namespace ScoutHelper.Utils;

public static partial class Utils {
// visible for testing
public static readonly IDictionary<Patch, uint> PatchMaxMarks = new Dictionary<Patch, uint>() {
{ Patch.ARR, 17 },
{ Patch.HW, 12 },
{ Patch.SB, 12 },
{ Patch.SHB, 12 },
{ Patch.EW, 16 },
}.VerifyEnumDictionary();

public static void CreateTooltip(string text, float width = 12f) {
ImGui.BeginTooltip();
Expand Down Expand Up @@ -46,7 +39,7 @@ string link

var variables = new Dictionary<string, string>() {
{ "#", trainList.Count.ToString() },
{ "#max", PatchMaxMarks[highestPatch].ToString() },
{ "#max", highestPatch.MaxMarks().ToString() },
{ "link", link },
{ "patch", highestPatch.ToString() },
{ "tracker", tracker },
Expand Down Expand Up @@ -124,3 +117,15 @@ private static IEnumerable<string> Tokenize(string s) {

#endregion
}

public static class UtilExtensions {
private static readonly IDictionary<Patch, uint> PatchMaxMarks = new Dictionary<Patch, uint>() {
{ Patch.ARR, 17 },
{ Patch.HW, 12 },
{ Patch.SB, 12 },
{ Patch.SHB, 12 },
{ Patch.EW, 16 },
}.VerifyEnumDictionary();

public static uint MaxMarks(this Patch patch) => PatchMaxMarks[patch];
}
106 changes: 76 additions & 30 deletions ScoutHelperTests/TestUtils/FsCheck/Arbs.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
using FsCheck;
using CSharpFunctionalExtensions;
using FsCheck;
using ScoutHelper;
using ScoutHelper.Models;
using ScoutHelper.Utils;
using static ScoutHelper.Utils.Utils;

namespace ScoutHelperTests.TestUtils.FsCheck;

public static class Arbs {
public static Arbitrary<string> NonEmptyString() =>
Arb.Default.NonEmptyString()
public static Arbitrary<string> String() =>
Arb.Default.UnicodeString()
.Generator
.Select(nes => nes.ToString())
.Select(s => s.ToString())
.ToArbitrary();

public static Arbitrary<T> OfEnum<T>() where T : struct, Enum => Gen.Elements(Enum.GetValues<T>()).ToArbitrary();

public static Arbitrary<List<T>> ListOf<T>(Gen<T> gen) =>
Gen.ListOf(gen)
.Select(list => list.ToList())
public static Arbitrary<string> NonEmptyString() =>
String()
.Generator
.Where(s => !string.IsNullOrEmpty(s))
.ToArbitrary();

public static Arbitrary<ICollection<T>> NonEmptyList<T>() =>
Arb.Default
.List<T>()
.Filter(list => 0 < list.Count)
.Generator
.Select(list => (ICollection<T>)list)
public static Arbitrary<IDictionary<K, V>> DictOf<K, V>(Gen<K> keyGen, Gen<V> valueGen) where K : notnull =>
FsCheckUtils.Zip(keyGen, valueGen)
.ListOf()
.Select(entryList => entryList.ToDict())
.ToArbitrary();

public static Arbitrary<T?> WithNulls<T>(Gen<T> gen) =>
RandomFreq(
gen.Select(value => (T?)value),
Gen.Constant((T?)default)
);

public static Arbitrary<T> OfEnum<T>() where T : struct, Enum => Gen.Elements(Enum.GetValues<T>()).ToArbitrary();

public static Arbitrary<IDictionary<K, V>> EnumDict<K, V>() where K : struct, Enum where V : notnull {
var enumValues = Enum.GetValues<K>();
return Gen.ListOf(enumValues.Length, Arb.Generate<V>())
Expand All @@ -48,7 +55,7 @@ public static Arbitrary<IDictionary<K, V>> PartialEnumDict<K, V>() where K : str
.ToArbitrary();

public static Arbitrary<CopyTemplateArb> CopyTemplate() {
var trainList = Arbs.ListOf(Gen.Constant(new TrainMob()));
var trainList = Gen.Constant(new TrainMob()).ListOf().ToArbitrary();
var tracker = Arb.Default.String();
var worldName = Arb.Default.String();
var highestPatch = OfEnum<Patch>();
Expand All @@ -59,7 +66,7 @@ public static Arbitrary<CopyTemplateArb> CopyTemplate() {
.Select(
arbs => (arbs, new List<(string?, string?)>() {
("{#}", arbs.a.Count.ToString()),
("{#max}", PatchMaxMarks[arbs.d].ToString()),
("{#max}", arbs.d.MaxMarks().ToString()),
("{tracker}", arbs.b),
("{world}", arbs.c),
("{patch}", arbs.d.ToString()),
Expand All @@ -68,20 +75,14 @@ public static Arbitrary<CopyTemplateArb> CopyTemplate() {
)
.SelectMany(
acc =>
Gen.Choose(0, 10)
.SelectMany(
f =>
Gen.Frequency(
Tuple.Create(f, Gen.Elements<(string?, string?)>(acc.Item2)),
Tuple.Create(
10 - f,
Arb.Generate<UnicodeString>()
.Select(s => s?.ToString()?.TrimEnd('\\'))
.Select(s => (s, s))
)
)
RandomFreq(
Gen.Elements<(string?, string?)>(acc.Item2),
String().Generator
.Select(s => ((string?)s)?.TrimEnd('\\'))
.Select(s => (s, s))
)
.ListOf()
.Generator
.Select(
chunks => (
string.Join(null, chunks.Select(chunk => chunk.Item1)),
Expand All @@ -104,12 +105,57 @@ public static Arbitrary<CopyTemplateArb> CopyTemplate() {
}

public record struct CopyTemplateArb(
List<TrainMob> TrainList,
IList<TrainMob> TrainList,
string Tracker,
string WorldName,
Patch HighestPatch,
string Link,
string Template,
string Expected
);

public static Arbitrary<Maybe<T>> MaybeArb<T>(Gen<T> gen, bool includeNulls = false) =>
(includeNulls ? WithNulls(gen).Generator : gen.Select(value => (T?)value!))
.Select(
value =>
Maybe
.From(value)
.Select(maybeValue => (T)maybeValue!)
)
.ToArbitrary();

public static Arbitrary<T> RandomFreq<T>(params Gen<T>[] gens) =>
Gen.Choose(0, 100)
.ListOf(gens.Length)
.SelectMany(
freqs =>
Gen.Frequency(freqs.Zip(gens).Select(f => Tuple.Create(f.First, f.Second)))
)
.ToArbitrary();
}

public static class ArbExtensions {
public static Arbitrary<IList<T>> ListOf<T>(this Arbitrary<T> arb) => arb.Generator.ListOf().ToArbitrary();

public static Arbitrary<IList<T>> NonEmptyListOf<T>(this Arbitrary<T> arb) =>
arb.Generator.NonEmptyListOf().ToArbitrary();

public static Arbitrary<IDictionary<K, V>> DictWith<K, V>(
this Arbitrary<K> keyArb,
Arbitrary<V> valueArb,
params K[] excluding
) where K : notnull =>
Arbs.DictOf(keyArb.Generator, valueArb.Generator)
.Generator
.Select(dict => dict.ToMutableDict())
.Select(
dict => {
excluding.ForEach(key => dict.Remove(key));
return dict.ToDict();
}
)
.ToArbitrary();

public static Arbitrary<Maybe<T>> ToMaybeArb<T>(this Arbitrary<T> arb, bool includeNulls = false) =>
Arbs.MaybeArb(arb.Generator, includeNulls);
}
2 changes: 0 additions & 2 deletions ScoutHelperTests/TestUtils/FsCheck/FsCheckUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ public static Property ForAll<A, B, C, D, E, F, G, H>(
Action<A, B, C, D, E, F, G, H> body
) => Prop.ForAll(Zip(a, b, c, d, e, f, g, h), x => body.Invoke(x.a, x.b, x.c, x.d, x.e, x.f, x.g, x.h));

public static Arbitrary<T> ToArbitrary<T>(this Gen<T> gen) => Arb.From(gen);

public static void Replay(this Property prop, int a, int b) {
var config = Configuration.QuickThrowOnFailure;
config.Replay = Random.StdGen.NewStdGen(a, b);
Expand Down
16 changes: 8 additions & 8 deletions ScoutHelperTests/Utils/CollectionExtensionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ public void ForEach_WithIndex_EmptyEnumerable() {

[Property]
public Property ForEach_NonEmptyEnumerable() => FsCheckUtils.ForAll(
Arbs.NonEmptyList<string?>(),
Arbs.String().NonEmptyListOf(),
testEnumerable => {
// DATA
int F(string? s) => s?.Length ?? 0;
var expected = testEnumerable.Select((Func<string?, int>)F).ToImmutableArray();
int F(string s) => s.Length;
var expected = testEnumerable.Select(F).ToImmutableArray();
// WHEN
var actual = new List<int>();
Expand All @@ -56,11 +56,11 @@ public Property ForEach_NonEmptyEnumerable() => FsCheckUtils.ForAll(

[Property]
public Property ForEach_WithIndex_NonEmptyEnumerable() => FsCheckUtils.ForAll(
Arbs.NonEmptyList<string?>(),
Arbs.String().NonEmptyListOf(),
testEnumerable => {
// DATA
int F(string? s, int i) => (s?.Length ?? 0) + i;
var expected = testEnumerable.Select((Func<string?, int, int>)F).ToImmutableArray();
int F(string s, int i) => s.Length + i;
var expected = testEnumerable.Select(F).ToImmutableArray();
// WHEN
var actual = new List<int>();
Expand All @@ -73,7 +73,7 @@ public Property ForEach_WithIndex_NonEmptyEnumerable() => FsCheckUtils.ForAll(

[Property]
public Property ForEach_ReturnsOriginalEnumerable() => FsCheckUtils.ForAll(
Arbs.NonEmptyList<string?>(),
Arbs.String().NonEmptyListOf(),
testEnumerable => {
// WHEN
var actualNumItems = 0;
Expand All @@ -87,7 +87,7 @@ public Property ForEach_ReturnsOriginalEnumerable() => FsCheckUtils.ForAll(

[Property]
public Property ForEach_WithIndex_ReturnsOriginalEnumerable() => FsCheckUtils.ForAll(
Arbs.NonEmptyList<string?>(),
Arbs.String().NonEmptyListOf(),
testEnumerable => {
// WHEN
var actualNumItems = 0;
Expand Down
91 changes: 91 additions & 0 deletions ScoutHelperTests/Utils/Functional/FunctionalExtensionsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using CSharpFunctionalExtensions;
using FluentAssertions;
using FsCheck;
using FsCheck.Xunit;
using JetBrains.Annotations;
using ScoutHelper;
using ScoutHelper.Utils.Functional;
using ScoutHelperTests.TestUtils.FsCheck;
using static ScoutHelperTests.TestUtils.FsCheck.FsCheckUtils;

namespace ScoutHelperTests.Utils.Functional;

[TestSubject(typeof(FunctionalExtensions))]
public class FunctionalExtensionsTest {
[Property]
public Property MaybeGet_MissingKey() => ForAll(
Arbs.String().DictWith(Arbs.String()),
Arbs.String(),
(arbitraryDict, lookupKey) => {
// DATA
var dict = arbitraryDict.Without(lookupKey);
// WHEN
var actual = dict.MaybeGet(lookupKey);
// THEN
actual.Should().Be(Maybe<string>.None);
}
);

[Property]
public Property MaybeGet_ContainsKey() => ForAll(
Arbs.String().DictWith(Arbs.String()),
Arbs.String(),
Arbs.String(),
(arbitraryDict, lookupKey, lookupValue) => {
// DATA
var dict = arbitraryDict.With((lookupKey, lookupValue));
// WHEN
var actual = dict.MaybeGet(lookupKey);
// THEN
actual.Should().Be(Maybe.From(lookupValue));
}
);

[Property]
public Property MaybeGet_EmptyDict() => ForAll(
Arbs.String(),
lookupKey => {
// DATA
var dict = new Dictionary<string, string>();
// WHEN
var actual = dict.MaybeGet(lookupKey);
// THEN
actual.Should().Be(Maybe<string>.None);
}
);

[Property]
public Property SelectMaybe_ExistingList() => ForAll(
Arbs.String().ToMaybeArb().NonEmptyListOf(),
(list) => {
// DATA
var expectedValues = list
.Where(maybe => !Maybe<string>.None.Equals(maybe))
.Select(value => value.Value);
// WHEN
var actual = list.SelectMaybe();
// THEN
actual.Should().BeEquivalentTo(expectedValues);
}
);

// select maybe
// reduce

// == result ==
// as pair
// for each error
// join
// select results
// select values
// to accresult
// with value
}

0 comments on commit 828d098

Please sign in to comment.