From ad9f0c5a53a8467ec5438aca5f1708a120a45540 Mon Sep 17 00:00:00 2001 From: ArcadeArchie <29282748+ArcadeArchie@users.noreply.github.com> Date: Fri, 30 Aug 2024 00:36:35 +0200 Subject: [PATCH 1/5] Bump version of packages --- src/ArcadePointsBot/ArcadePointsBot.csproj | 42 +++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/ArcadePointsBot/ArcadePointsBot.csproj b/src/ArcadePointsBot/ArcadePointsBot.csproj index abae410..b7dacc1 100644 --- a/src/ArcadePointsBot/ArcadePointsBot.csproj +++ b/src/ArcadePointsBot/ArcadePointsBot.csproj @@ -17,30 +17,30 @@ - - - - - + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive From ba5a8f5584da88c33d889c06e99b90602099bc54 Mon Sep 17 00:00:00 2001 From: ArcadeArchie <29282748+ArcadeArchie@users.noreply.github.com> Date: Mon, 2 Sep 2024 01:12:22 +0200 Subject: [PATCH 2/5] checkin --- ArcadePointsBot.sln | 6 ++ src/ArcadePointsBot/ArcadePointsBot.csproj | 2 +- .../Abstractions/Repositories/IRepository.cs | 3 +- .../Data/Repositories/DataEntityRepository.cs | 4 +- .../Properties/AssemblyInfo.cs | 4 ++ src/ArcadePointsBot/TwitchWorker.cs | 18 +++--- src/ArcadePointsBot/Util/StringUtils.cs | 60 +++++++++++++++++++ src/ArcadePointsBot/appsettings.json | 3 +- .../ArcadePointsBot.Tests.csproj | 33 ++++++++++ .../ArcadePointsBot.Tests/StringUtilTests.cs | 59 ++++++++++++++++++ 10 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 src/ArcadePointsBot/Properties/AssemblyInfo.cs create mode 100644 src/ArcadePointsBot/Util/StringUtils.cs create mode 100644 tests/ArcadePointsBot.Tests/ArcadePointsBot.Tests.csproj create mode 100644 tests/ArcadePointsBot.Tests/StringUtilTests.cs diff --git a/ArcadePointsBot.sln b/ArcadePointsBot.sln index 9ccc626..7084e73 100644 --- a/ArcadePointsBot.sln +++ b/ArcadePointsBot.sln @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM .github\ISSUE_TEMPLATE\config.yml = .github\ISSUE_TEMPLATE\config.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcadePointsBot.Tests", "tests\ArcadePointsBot.Tests\ArcadePointsBot.Tests.csproj", "{631CC0FD-4906-41DE-841C-856D3330709A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +31,10 @@ Global {BB6C1FEA-EA74-47AD-AC7A-B619C243DE3C}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB6C1FEA-EA74-47AD-AC7A-B619C243DE3C}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB6C1FEA-EA74-47AD-AC7A-B619C243DE3C}.Release|Any CPU.Build.0 = Release|Any CPU + {631CC0FD-4906-41DE-841C-856D3330709A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {631CC0FD-4906-41DE-841C-856D3330709A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {631CC0FD-4906-41DE-841C-856D3330709A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {631CC0FD-4906-41DE-841C-856D3330709A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ArcadePointsBot/ArcadePointsBot.csproj b/src/ArcadePointsBot/ArcadePointsBot.csproj index b7dacc1..07acd3a 100644 --- a/src/ArcadePointsBot/ArcadePointsBot.csproj +++ b/src/ArcadePointsBot/ArcadePointsBot.csproj @@ -39,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ArcadePointsBot/Data/Abstractions/Repositories/IRepository.cs b/src/ArcadePointsBot/Data/Abstractions/Repositories/IRepository.cs index 4cc2ffe..2f6c772 100644 --- a/src/ArcadePointsBot/Data/Abstractions/Repositories/IRepository.cs +++ b/src/ArcadePointsBot/Data/Abstractions/Repositories/IRepository.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text; using System.Threading.Tasks; namespace ArcadePointsBot.Data.Abstractions.Repositories; @@ -116,7 +115,7 @@ public interface IReadRepository /// /// Condition /// Query - IQueryable GetBy(Expression> predicate); + IEnumerable GetBy(Func predicate); /// /// Retrieves the first entity from data store that satisfies the given predicate diff --git a/src/ArcadePointsBot/Data/Repositories/DataEntityRepository.cs b/src/ArcadePointsBot/Data/Repositories/DataEntityRepository.cs index f958ca2..fa4d019 100644 --- a/src/ArcadePointsBot/Data/Repositories/DataEntityRepository.cs +++ b/src/ArcadePointsBot/Data/Repositories/DataEntityRepository.cs @@ -72,9 +72,9 @@ public IQueryable GetAll() return GetEntities(); } - public IQueryable GetBy(Expression> predicate) + public IEnumerable GetBy(Func predicate) { - return GetEntities().Where(predicate); + return GetEntities().ToList().Where(predicate); } public T? GetFirst(Expression> predicate) diff --git a/src/ArcadePointsBot/Properties/AssemblyInfo.cs b/src/ArcadePointsBot/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a645634 --- /dev/null +++ b/src/ArcadePointsBot/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ArcadePointsBot.Tests")] +public class AssemblyInfo; diff --git a/src/ArcadePointsBot/TwitchWorker.cs b/src/ArcadePointsBot/TwitchWorker.cs index 02f174f..f1572af 100644 --- a/src/ArcadePointsBot/TwitchWorker.cs +++ b/src/ArcadePointsBot/TwitchWorker.cs @@ -7,6 +7,7 @@ using ArcadePointsBot.Auth; using ArcadePointsBot.Domain.Rewards; using ArcadePointsBot.Infrastructure.Interop; +using ArcadePointsBot.Util; using ArcadePointsBot.Views; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; @@ -24,6 +25,7 @@ namespace ArcadePointsBot; public class TwitchWorker : BackgroundService, INotifyPropertyChanged { + private const double SimilarityThreshold = .6; private readonly Keyboard _keyboard; private readonly Mouse _mouse; private readonly ILogger _logger; @@ -83,18 +85,20 @@ public TwitchWorker(IServiceProvider provider) PropertyChanged += ChangeActiveRewards; } - private async void ChangeActiveRewards(object? sender, PropertyChangedEventArgs e) + private void ChangeActiveRewards(object? sender, PropertyChangedEventArgs e) { + if (e.PropertyName is not nameof(CurrentCategory)) + return; if (!isLive) return; - var toDisableIds = await _rewardRepository - .GetBy(x => x.IsEnabled && x.Category != null && x.Category != CurrentCategory) + var toDisableIds = _rewardRepository + .GetBy(x => x.IsEnabled && x.Category != null && !StringUtils.TestSimilarity(x.Category, CurrentCategory, SimilarityThreshold)) .Select(x => x.Id) - .ToListAsync(); - var toEnableIds = await _rewardRepository - .GetBy(x => !x.IsEnabled && x.Category != null && x.Category == CurrentCategory) + .ToList(); + var toEnableIds = _rewardRepository + .GetBy(x => !x.IsEnabled && x.Category != null && StringUtils.TestSimilarity(x.Category, CurrentCategory, SimilarityThreshold)) .Select(x => x.Id) - .ToListAsync(); + .ToList(); Dispatcher.UIThread.Post(async () => { diff --git a/src/ArcadePointsBot/Util/StringUtils.cs b/src/ArcadePointsBot/Util/StringUtils.cs new file mode 100644 index 0000000..d4f590b --- /dev/null +++ b/src/ArcadePointsBot/Util/StringUtils.cs @@ -0,0 +1,60 @@ +using System; +using TwitchLib.Api.Helix.Models.Soundtrack; + +namespace ArcadePointsBot.Util; +internal static partial class StringUtils +{ + public static double CalculateSimilarity(string? source, string? target) + { + if ((source == null) || (target == null)) return 0.0; + if ((source.Length == 0) || (target.Length == 0)) return 0.0; + if (source == target) return 1.0; + + int stepsToSame = ComputeLevenshteinDistance(source, target); + return (1.0 - ((double)stepsToSame / (double)Math.Max(source.Length, target.Length))); + } + + public static double CalculateSimilarity(string? source, string? target, bool roundUp = false) + { + var similarity = CalculateSimilarity(source, target); + if (roundUp) + return double.Round(similarity, 2); + else + return similarity; + } + + public static bool TestSimilarity(string? source, string? target, double minThreshold, bool roundUp = false) + { + var similarity = CalculateSimilarity(source, target, roundUp); + return similarity >= minThreshold; + } + public static bool TestSimilarity(string? source, string? target, double minThreshold) + { + var similarity = CalculateSimilarity(source, target); + return similarity >= minThreshold; + } + + private static int ComputeLevenshteinDistance(string s, string t) + { + int n = s.Length; + int m = t.Length; + int[,] d = new int[n + 1, m + 1]; + + // initialize the top and right of the table to 0, 1, 2, ... + for (int i = 0; i <= n; d[i, 0] = i++) ; + for (int j = 1; j <= m; d[0, j] = j++) ; + + for (int i = 1; i <= n; i++) + { + for (int j = 1; j <= m; j++) + { + int cost = (t[j - 1] == s[i - 1]) ? 0 : 1; + int min1 = d[i - 1, j] + 1; + int min2 = d[i, j - 1] + 1; + int min3 = d[i - 1, j - 1] + cost; + d[i, j] = Math.Min(Math.Min(min1, min2), min3); + } + } + return d[n, m]; + } +} diff --git a/src/ArcadePointsBot/appsettings.json b/src/ArcadePointsBot/appsettings.json index 6d6d813..6aa0534 100644 --- a/src/ArcadePointsBot/appsettings.json +++ b/src/ArcadePointsBot/appsettings.json @@ -9,7 +9,8 @@ "IdentityModel.OidcClient.OidcClient": "Warning", "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Warning", - "Microsoft.EntityFrameworkCore.Infrastructure": "Warning" + "Microsoft.EntityFrameworkCore.Infrastructure": "Warning", + "TwitchLib.Api.Core.HttpCallHandlers.TwitchHttpClient": "Verbose" } }, "WriteTo": [ diff --git a/tests/ArcadePointsBot.Tests/ArcadePointsBot.Tests.csproj b/tests/ArcadePointsBot.Tests/ArcadePointsBot.Tests.csproj new file mode 100644 index 0000000..a95c695 --- /dev/null +++ b/tests/ArcadePointsBot.Tests/ArcadePointsBot.Tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/tests/ArcadePointsBot.Tests/StringUtilTests.cs b/tests/ArcadePointsBot.Tests/StringUtilTests.cs new file mode 100644 index 0000000..4bc4208 --- /dev/null +++ b/tests/ArcadePointsBot.Tests/StringUtilTests.cs @@ -0,0 +1,59 @@ +using ArcadePointsBot.Util; +using Xunit.Abstractions; + +namespace ArcadePointsBot.Tests; + +public class StringUtilTests(ITestOutputHelper testLogger) +{ + const double SimilarityThresholdMin = 0.6; + + [Theory] + [MemberData(nameof(CalcTestData))] + public void TestCalculateSimilarity(string a, string b, double min, double max) + { + var result = StringUtils.CalculateSimilarity(a, b); + testLogger.WriteLine("Calculated similarity: {0}", result); + Assert.InRange(result, min, max); + } + + [Theory] + [MemberData(nameof(CalcTestData))] + public void TestCalculateSimilarityWithRounding(string a, string b, double min, double max) + { + var result = StringUtils.CalculateSimilarity(a, b, true); + testLogger.WriteLine("Calculated similarity: {0}", result); + Assert.InRange(result, min, max); + } + + [Theory] + [MemberData(nameof(ComparisonTestData))] + public void TestSimilarity(string a, string b, double threshold, bool expected) + { + var result = StringUtils.TestSimilarity(a, b, threshold, true); + testLogger.WriteLine("Calculated similarity: {0}", result); + Assert.Equal(expected, result); + } + [Theory] + [MemberData(nameof(ComparisonTestData))] + public void TestSimilarityWithRounding(string a, string b, double threshold, bool expected) + { + var result = StringUtils.TestSimilarity(a, b, threshold, true); + testLogger.WriteLine("Calculated similarity: {0}", result); + Assert.Equal(expected, result); + } + + public static IEnumerable CalcTestData() + { + yield return new object[] { "Escape From Tarkov", "Escape From Tarkov: Arena", SimilarityThresholdMin, 0.9 }; + yield return new object[] { "Escape From Tarkov", "Escape From", SimilarityThresholdMin, 0.9 }; + yield return new object[] { "Escape From Tarkov", "Escape Torkiv", SimilarityThresholdMin, 0.9 }; + yield return new object[] { "Rocket League", "Escape From Tarkov", 0, SimilarityThresholdMin }; + } + public static IEnumerable ComparisonTestData() + { + yield return new object[] { "Escape From Tarkov", "Escape From Tarkov: Arena", SimilarityThresholdMin, true }; + yield return new object[] { "Escape From Tarkov", "Escape From", SimilarityThresholdMin, true }; + yield return new object[] { "Escape From Tarkov", "Escape Torkiv", SimilarityThresholdMin, true }; + yield return new object[] { "Rocket League", "Escape From Tarkov", SimilarityThresholdMin, false }; + } +} \ No newline at end of file From a5285693783d7ee639bb7c03f215b0c55a206eeb Mon Sep 17 00:00:00 2001 From: Nico <29282748+ArcadeArchie@users.noreply.github.com> Date: Mon, 2 Sep 2024 01:22:06 +0200 Subject: [PATCH 3/5] Create ci.yaml add build and test to CI as a requirement for PRs --- .github/workflows/ci.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..04d1a11 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,26 @@ +name: "CI" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build -c Release --no-restore + - name: Test + run: dotnet test --no-restore --verbosity normal #--collect:"XPlat Code Coverage" + #- name: Upload coverage to Codecov + # uses: codecov/codecov-action@v3 + # with: + # token: ${{ secrets.CODE_COV_TOKEN }} From 34608a74c3a76ea19609e686b35c361024b9e3d5 Mon Sep 17 00:00:00 2001 From: Nico <29282748+ArcadeArchie@users.noreply.github.com> Date: Mon, 2 Sep 2024 01:27:19 +0200 Subject: [PATCH 4/5] Update ci.yaml Updated GH actions to thier latest version --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 04d1a11..12cdf2d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,9 +9,9 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Restore dependencies From 70352ab3e41b94be60a7e24cb4734b02dfc83b06 Mon Sep 17 00:00:00 2001 From: Nico <29282748+ArcadeArchie@users.noreply.github.com> Date: Mon, 2 Sep 2024 01:33:21 +0200 Subject: [PATCH 5/5] Update ci.yaml Use matrix strategies to test for windows and linux Upload test results as artifact --- .github/workflows/ci.yaml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 12cdf2d..7003d1c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,8 +6,11 @@ on: pull_request: branches: [ "master" ] jobs: - build: - runs-on: windows-latest + build: + strategy: + matrix: + os: [windows-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup .NET @@ -19,8 +22,11 @@ jobs: - name: Build run: dotnet build -c Release --no-restore - name: Test - run: dotnet test --no-restore --verbosity normal #--collect:"XPlat Code Coverage" - #- name: Upload coverage to Codecov - # uses: codecov/codecov-action@v3 - # with: - # token: ${{ secrets.CODE_COV_TOKEN }} + run: dotnet test --no-restore --logger trx --results-directory "TestResults-${{ matrix.os }}" --verbosity normal + - name: Upload dotnet test results + uses: actions/upload-artifact@v4 + with: + name: dotnet-results-${{ matrix.os }} + path: TestResults-${{ matrix.os }} + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }}