diff --git a/src/RepoM.Api/Git/FileRepositoryStore.cs b/src/RepoM.Api/Git/FileRepositoryStore.cs index 87b99329..02a382c0 100644 --- a/src/RepoM.Api/Git/FileRepositoryStore.cs +++ b/src/RepoM.Api/Git/FileRepositoryStore.cs @@ -47,7 +47,7 @@ public void Set(IEnumerable paths) } } - private IEnumerable Get(string file) + private string[] Get(string file) { if (!_fileSystem.File.Exists(file)) { diff --git a/src/RepoM.App/Converters/UtcToHumanizedLocalDateTimeConverter.cs b/src/RepoM.App/Converters/UtcToHumanizedLocalDateTimeConverter.cs index 0cfc68f8..e75e9226 100644 --- a/src/RepoM.App/Converters/UtcToHumanizedLocalDateTimeConverter.cs +++ b/src/RepoM.App/Converters/UtcToHumanizedLocalDateTimeConverter.cs @@ -7,15 +7,15 @@ namespace RepoM.App.Converters; public class UtcToHumanizedLocalDateTimeConverter : IValueConverter { - private static readonly IHumanizer _humanizer = new HardcodededMiniHumanizer(SystemClock.Instance); + private static readonly HardcodededMiniHumanizer _humanizer = new(SystemClock.Instance); - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - DateTime date = DateTime.SpecifyKind(DateTime.Parse(value.ToString() ?? string.Empty), DateTimeKind.Utc).ToLocalTime(); + DateTime date = DateTime.SpecifyKind(DateTime.Parse(value?.ToString() ?? string.Empty), DateTimeKind.Utc).ToLocalTime(); return _humanizer.HumanizeTimestamp(date); } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); } diff --git a/src/RepoM.App/RepositoryOrdering/RepositoryComparerCompositionFactory.cs b/src/RepoM.App/RepositoryOrdering/RepositoryComparerCompositionFactory.cs index 067c624c..a33b531f 100644 --- a/src/RepoM.App/RepositoryOrdering/RepositoryComparerCompositionFactory.cs +++ b/src/RepoM.App/RepositoryOrdering/RepositoryComparerCompositionFactory.cs @@ -9,24 +9,30 @@ namespace RepoM.App.RepositoryOrdering; internal class RepositoryComparerCompositionFactory : IRepositoryComparerFactory { private readonly Container _container; - private readonly ILogger _logger; + private readonly ILogger _logger; - public RepositoryComparerCompositionFactory(Container container, ILogger logger) + public RepositoryComparerCompositionFactory(Container container, ILogger logger) { _container = container ?? throw new ArgumentNullException(nameof(container)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// Thrown when repository comparer cannot be created. public IRepositoryComparer Create(IRepositoriesComparerConfiguration configuration) { try { return CreateInner(configuration); } + catch (ActivationException e) + { + _logger.LogCritical(e, "Could not create a IRepositoryComparer for configuration type '{Configuration}'", configuration); + throw new InvalidOperationException($"Could not create a IRepositoryComparer for configuration type '{configuration}'", e); + } catch (Exception e) { - _logger.LogCritical(e, "Could not create a IRepositoryComparer for configuration type '{configuration}'", configuration); - throw; + _logger.LogCritical(e, "Factory could not create instance of IRepositoryComparer '{Message}'", e.Message); + throw new InvalidOperationException($"Factory could not create instance of IRepositoryComparer '{e.Message}'", e); } } diff --git a/src/RepoM.Core.Plugin/Repository/RepositoryContext.cs b/src/RepoM.Core.Plugin/Repository/RepositoryContext.cs deleted file mode 100644 index 5501d3fd..00000000 --- a/src/RepoM.Core.Plugin/Repository/RepositoryContext.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace RepoM.Core.Plugin.Repository; - -public sealed class RepositoryContext -{ - public RepositoryContext() - { - Repository = null; - } - - public RepositoryContext(IRepository repository) - { - Repository = repository; - } - - public static RepositoryContext EmptyContext { get; } = new RepositoryContext(); - - public IRepository? Repository { get; } - - public static RepositoryContext Create(IRepository? repository) - { - return repository == null ? EmptyContext : new RepositoryContext(repository); - } -} \ No newline at end of file diff --git a/tests/.editorconfig b/tests/.editorconfig index 3d5ebe0d..b039cc72 100644 --- a/tests/.editorconfig +++ b/tests/.editorconfig @@ -9,6 +9,11 @@ root = false # CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array dotnet_diagnostic.CA1861.severity = none +# CA1859: Using concrete types avoids virtual or interface call overhead and enables inlining. +dotnet_diagnostic.CA1859.severity = none + + + ############################### # ReSharper # ############################### diff --git a/tests/RepoM.App.Tests/RepositoryOrdering/RepositoryComparerCompositionFactoryTests.cs b/tests/RepoM.App.Tests/RepositoryOrdering/RepositoryComparerCompositionFactoryTests.cs new file mode 100644 index 00000000..c3e0be73 --- /dev/null +++ b/tests/RepoM.App.Tests/RepositoryOrdering/RepositoryComparerCompositionFactoryTests.cs @@ -0,0 +1,104 @@ +namespace RepoM.App.Tests.RepositoryOrdering; + +using System; +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using RepoM.App.RepositoryOrdering; +using RepoM.Core.Plugin.RepositoryOrdering; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; +using SimpleInjector; +using Xunit; + +public class RepositoryComparerCompositionFactoryTests +{ + private readonly Container _container = new(); + private readonly ILogger _logger = A.Fake(); + + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new RepositoryComparerCompositionFactory(new Container(), null!); + Func act2 = () => new RepositoryComparerCompositionFactory(null!, A.Dummy()); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrow_WhenContainerCannotCreateInstance() + { + // arrange + var sut = new RepositoryComparerCompositionFactory(_container, _logger); + + // act + Func act = () => sut.Create(new DummyConfiguration()); + + // assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldThrow_WhenCreatedFactoryThrows() + { + // arrange + _container.RegisterSingleton, FailingRepositoryComparerFactoryDummyConfiguration>(); + var sut = new RepositoryComparerCompositionFactory(_container, _logger); + + // act + Func act = () => sut.Create(new DummyConfiguration()); + + // assert + act.Should().Throw(); + } + + [Fact] + public void Create_ShouldReturnInstance_WhenFactoryCreatesInstance() + { + // arrange + IRepositoryComparer comparer = A.Fake(); + _container.RegisterSingleton>(() => new RepositoryComparerFactoryDummyConfiguration(comparer)); + var sut = new RepositoryComparerCompositionFactory(_container, _logger); + + // act + IRepositoryComparer result = sut.Create(new DummyConfiguration()); + + // assert + result.Should().Be(comparer); + } +} + +public class FailingRepositoryComparerFactoryDummyConfiguration : IRepositoryComparerFactory +{ + public IRepositoryComparer Create(DummyConfiguration configuration) + { + throw new NotImplementedException("Thrown by test"); + } +} + +public class RepositoryComparerFactoryDummyConfiguration : IRepositoryComparerFactory +{ + private readonly IRepositoryComparer _instance; + + public RepositoryComparerFactoryDummyConfiguration(IRepositoryComparer instance) + { + _instance = instance; + } + public IRepositoryComparer Create(DummyConfiguration configuration) + { + return _instance; + } +} + +public class DummyConfiguration : IRepositoriesComparerConfiguration +{ + public string Type + { + get => "DUMMY"; + set => _ = value; + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.AzureDevOps.Tests/RepositoryFiltering/HasPullRequestsMatcherTest.cs b/tests/RepoM.Plugin.AzureDevOps.Tests/RepositoryFiltering/HasPullRequestsMatcherTest.cs index cdc5baca..77af9070 100644 --- a/tests/RepoM.Plugin.AzureDevOps.Tests/RepositoryFiltering/HasPullRequestsMatcherTest.cs +++ b/tests/RepoM.Plugin.AzureDevOps.Tests/RepositoryFiltering/HasPullRequestsMatcherTest.cs @@ -12,13 +12,21 @@ namespace RepoM.Plugin.AzureDevOps.Tests.RepositoryFiltering; public class HasPullRequestsMatcherTest { - private readonly IRepository _repository; - private readonly IAzureDevOpsPullRequestService _azureDevOpsPullRequestService; + private readonly IRepository _repository = A.Fake(); + private readonly IAzureDevOpsPullRequestService _azureDevOpsPullRequestService = A.Fake(); - public HasPullRequestsMatcherTest() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Ctor_ShouldThrow_WhenArgumentNull(bool ignoreCase) { - _repository = A.Fake(); - _azureDevOpsPullRequestService = A.Fake(); + // arrange + + // act + Func act1 = () => new HasPullRequestsMatcher(null!, ignoreCase); + + // assert + act1.Should().Throw(); } [Theory] @@ -95,11 +103,11 @@ public static IEnumerable InvalidTerms { get { - yield return new object[] { new TermRange("has", null, true, "x", false), }; - yield return new object[] { new SimpleTerm(string.Empty, string.Empty), }; - yield return new object[] { new SimpleTerm(string.Empty, "data"), }; - yield return new object[] { new SimpleTerm("is", "data"), }; - yield return new object[] { new SimpleTerm("has", "wrong-value"), }; + yield return [new TermRange("has", null, true, "x", false),]; + yield return [new SimpleTerm(string.Empty, string.Empty),]; + yield return [new SimpleTerm(string.Empty, "data"),]; + yield return [new SimpleTerm("is", "data"),]; + yield return [new SimpleTerm("has", "wrong-value"),]; } } @@ -107,12 +115,12 @@ public static IEnumerable ValidTerms { get { - yield return new object[] { new SimpleTerm("has", "prs"), }; - yield return new object[] { new SimpleTerm("has", "pr"), }; - yield return new object[] { new SimpleTerm("has", "pull-request"), }; - yield return new object[] { new SimpleTerm("has", "pull-requests"), }; - yield return new object[] { new SimpleTerm("has", "pullrequest"), }; - yield return new object[] { new SimpleTerm("has", "pullrequests"), }; + yield return [new SimpleTerm("has", "prs"),]; + yield return [new SimpleTerm("has", "pr"),]; + yield return [new SimpleTerm("has", "pull-request"),]; + yield return [new SimpleTerm("has", "pull-requests"),]; + yield return [new SimpleTerm("has", "pullrequest"),]; + yield return [new SimpleTerm("has", "pullrequests"),]; } } } \ No newline at end of file diff --git a/tests/RepoM.Plugin.LuceneQueryParser.Tests/LuceneQueryParserPackageTest.cs b/tests/RepoM.Plugin.LuceneQueryParser.Tests/LuceneQueryParserPackageTest.cs index 4b0a7c06..a26803c9 100644 --- a/tests/RepoM.Plugin.LuceneQueryParser.Tests/LuceneQueryParserPackageTest.cs +++ b/tests/RepoM.Plugin.LuceneQueryParser.Tests/LuceneQueryParserPackageTest.cs @@ -46,7 +46,7 @@ public void RegisterServices_ShouldRegisterLuceneQueryParserAsCollection() INamedQueryParser[] instances = _container.GetAllInstances().ToArray(); // assert - _ = instances.Should().HaveCount(1).And.AllBeOfType(); + _ = instances.Should().ContainSingle().And.AllBeOfType(); } [Fact] diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScoreCalculatorTest.cs b/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScoreCalculatorTest.cs index 0fab29ec..d7c42325 100644 --- a/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScoreCalculatorTest.cs +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScoreCalculatorTest.cs @@ -61,6 +61,22 @@ public UsageScoreCalculatorTest() _sut = new UsageScoreCalculator(_service, _calculatorClock, _defaultConfig); } + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new UsageScoreCalculator(A.Dummy(), A.Dummy(), null!); + Func act2 = () => new UsageScoreCalculator(A.Dummy(), null!, _defaultConfig); + Func act3 = () => new UsageScoreCalculator(null!, A.Dummy(), _defaultConfig); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + act3.Should().Throw(); + } + [Fact] public void Score_ShouldUseRecordings_WhenCalculating() { diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScorerFactoryTest.cs b/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScorerFactoryTest.cs index 5e38bec1..8adda4b1 100644 --- a/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScorerFactoryTest.cs +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScorerFactoryTest.cs @@ -1,5 +1,6 @@ namespace RepoM.Plugin.Statistics.Tests.Ordering; +using System; using FakeItEasy; using FluentAssertions; using RepoM.Core.Plugin.Common; @@ -18,6 +19,20 @@ public UsageScorerFactoryTest() _service = new StatisticsService(_clock); } + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new UsageScorerFactory(null!, A.Dummy()); + Func act2 = () => new UsageScorerFactory(_service, null!); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + } + [Fact] public void Create_ShouldReturnInstanceOfUsageScoreCalculator() { diff --git a/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs b/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs index 23850eab..6170c4ca 100644 --- a/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs +++ b/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs @@ -11,6 +11,20 @@ namespace RepoM.Plugin.Statistics.Tests; public class RepositoryStatisticsTest { + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new RepositoryStatistics(null!, A.Dummy()); + Func act2 = () => new RepositoryStatistics("", null!); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + } + [Fact] public void Recordings_ShouldBeEmpty_WhenConstructed() { diff --git a/tests/RepoM.Plugin.WebBrowser.Tests/WebBrowserServiceTest.cs b/tests/RepoM.Plugin.WebBrowser.Tests/WebBrowserServiceTest.cs index 1a179280..9fd8c7fa 100644 --- a/tests/RepoM.Plugin.WebBrowser.Tests/WebBrowserServiceTest.cs +++ b/tests/RepoM.Plugin.WebBrowser.Tests/WebBrowserServiceTest.cs @@ -102,14 +102,38 @@ public void OpenUrl_ShouldStartProcess() } [Fact] - public void OpenUrl_WithProfile_ShouldStartProcess() + public void OpenUrl_WithProfile_ShouldStartProcess_WhenUrlPlaceholderIsPresent() { // arrange var config = new WebBrowserConfiguration { Profiles = new() { - { "Private", new BrowserProfileConfig { BrowserName = "Edge", CommandLineArguments = "\"--profile 23 \" {url}", } }, + { "Private", new BrowserProfileConfig { BrowserName = "Edge", CommandLineArguments = "\"--profile 23 \" {url} dummy", } }, + }, + Browsers = new() + { + { "Edge", "msedge.exe" }, + }, + }; + var sut = new DummyWebBrowserService(config); + + // act + sut.OpenUrl("https://google.com", "Private"); + + // assert + sut.StartProcessCalled.Should().BeEquivalentTo("msedge.exe - \"--profile 23 \" https://google.com dummy"); + } + + [Fact] + public void OpenUrl_WithProfile_ShouldStartProcess_WhenUrlPlaceholderIsNotPresent() + { + // arrange + var config = new WebBrowserConfiguration + { + Profiles = new() + { + { "Private", new BrowserProfileConfig { BrowserName = "Edge", CommandLineArguments = "\"--profile 23 \"", } }, }, Browsers = new() { @@ -172,6 +196,32 @@ public void OpenUrl_WithProfile_ShouldStartProcessWithoutProfile_WhenBrowserNotE // assert sut.StartProcessCalled.Should().BeEquivalentTo("https://google.com - "); } + + [Theory] + [InlineData(null)] + [InlineData(" ")] + public void OpenUrl_WithProfile_ShouldStartProcessWithProfile_WhenBrowserExistsAndCommandLineArgsAreEmpty(string? commandLineArguments) + { + // arrange + var config = new WebBrowserConfiguration + { + Profiles = new() + { + { "Private", new BrowserProfileConfig { BrowserName = "Edge", CommandLineArguments = commandLineArguments, } }, + }, + Browsers = new() + { + { "Edge", "msedge.exe" }, + }, + }; + var sut = new DummyWebBrowserService(config); + + // act + sut.OpenUrl("https://google.com", "Private"); + + // assert + sut.StartProcessCalled.Should().BeEquivalentTo("msedge.exe - https://google.com"); + } } file class DummyWebBrowserService : WebBrowserService