diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5135f7c..f3ea1fc 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -3,14 +3,17 @@ run-name: CIBuild_${{ github.event_name }}_${{ github.ref_name }}_${{ github.run env: PKG_MAJOR_VERSION: 1.2 - PROJECT_NAME: 'DNX.Extensions' + PROJECT_NAME: DNX.Extensions DOTNET_VERSION: 8.0.x NUGET_VERSION: 5.x BUILD_CONFIG: Release BUILD_PLATFORM: Any CPU PACK_PARAMETERS: '' + COVERAGE_WARNING_THRESHOLD: 60 + COVERAGE_ERROR_THRESHOLD: 80 NUGET_OUTPUT_FOLDER: nupkgs - BRANCH_RELEASE_CANDIDATE: rc/** + BRANCH_PREFIX_RELEASE_CANDIDATE: rc/ + BRANCH_PREFIX_PUBLISH_CANDIDATE: beta/ on: push: @@ -53,6 +56,21 @@ jobs: echo "package_suffix=${package_suffix}" >> $GITHUB_ENV + - name: Determing GitHub Releasing + id: should_release + run: | + should_release=false + + if [ "${{ github.ref }}" == 'refs/heads/main' ] + then + should_release=true + elif [[ "${{ github.ref }}" == refs/heads/${{ env.BRANCH_PREFIX_RELEASE_CANDIDATE }}* ]] + then + should_publish=true + fi + + echo "should_release=${should_release}" >> $GITHUB_ENV + - name: Determine package publishing id: should_publish run: | @@ -64,7 +82,7 @@ jobs: elif [ "${{ github.ref }}" == "refs/heads/main" ] then should_publish=true - elif [[ "${{ github.ref }}" == ${{ env.BRANCH_RELEASE_CANDIDATE }}* ]] + elif [[ "${{ github.ref }}" == refs/heads/${{ env.BRANCH_PREFIX_PUBLISH_CANDIDATE }}* ]] then should_publish=true fi @@ -83,6 +101,10 @@ jobs: id: package_version run: echo "package_version=${{ env.assembly_version }}${{ env.package_suffix }}" >> $GITHUB_ENV + - name: Show Configuration + id: show_configuration + run: env + outputs: assembly_version: ${{ env.assembly_version }} product_version: ${{ env.product_version }} @@ -114,10 +136,22 @@ jobs: run: dotnet build --no-restore --configuration ${{ env.BUILD_CONFIG }} /p:"Platform=${{ env.BUILD_PLATFORM }}" /p:"Version=${{ needs.setup.outputs.product_version }}" /p:"AssemblyVersion=${{ needs.setup.outputs.assembly_version }}" - name: Test - run: dotnet test --no-restore --no-build --configuration ${{ env.BUILD_CONFIG }} --verbosity normal --collect:"XPlat Code Coverage" + run: dotnet test --no-restore --no-build --configuration ${{ env.BUILD_CONFIG }} --verbosity normal --collect:"XPlat Code Coverage" --logger:trx;LogFileName=TestOutput.trx + + - name: Test Results Summary + uses: bibipkins/dotnet-test-reporter@v1.4.0 + if: success() || failure() + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + comment-title: 'Unit Test Results' + results-path: "**/*.trx" + coverage-type: cobertura + #coverage-path: "**/coverage.cobertura.xml" + #coverage-threshold: ${{ env.COVERAGE_WARNING_THRESHOLD }} - name: Code Coverage Report uses: irongut/CodeCoverageSummary@v1.3.0 + if: success() || failure() with: filename: "**/coverage.cobertura.xml" badge: true @@ -127,7 +161,10 @@ jobs: hide_complexity: false indicators: true output: both - thresholds: '60 80' + thresholds: '${{ env.COVERAGE_ERROR_THRESHOLD }} ${{ env.COVERAGE_WARNING_THRESHOLD }}' + + - name: Output Code Coverage + run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY - name: Pull Request - Add Coverage Comment uses: marocchino/sticky-pull-request-comment@v2 @@ -157,7 +194,7 @@ jobs: ## Generate a Release and Tag in git release: name: Create GitHub Release - if: github.ref == 'refs/heads/main' && success() + if: needs.setup.outputs.should_release == 'true' && success() needs: - setup @@ -169,9 +206,14 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: nuget_output + path: nuget + - name: Build Changelog id: build_changelog - uses: mikepenz/release-changelog-builder-action@v3.4.0 + uses: mikepenz/release-changelog-builder-action@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -184,7 +226,7 @@ jobs: artifacts: '**/*.nupkg' - name: Tag git - uses: pkgdeps/git-tag-action@v2.0.5 + uses: pkgdeps/git-tag-action@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} github_repo: ${{ github.repository }} @@ -196,7 +238,7 @@ jobs: ## Publish to NuGet publish: name: Publish to NuGet - if: needs.setup.outputs.should_publish == 'true' + if: needs.setup.outputs.should_publish == 'true' && success() needs: - setup @@ -207,7 +249,7 @@ jobs: steps: - name: Install NuGet - uses: NuGet/setup-nuget@v1.1.1 + uses: NuGet/setup-nuget@v2 with: nuget-api-key: ${{ secrets.NUGET_API_KEY }} nuget-version: ${{ env.NUGET_VERSION }} diff --git a/src/DNX.Extensions/Arrays/ArrayExtensions.cs b/src/DNX.Extensions/Arrays/ArrayExtensions.cs new file mode 100644 index 0000000..d1e7aa7 --- /dev/null +++ b/src/DNX.Extensions/Arrays/ArrayExtensions.cs @@ -0,0 +1,45 @@ +using System; + +#pragma warning disable 1591 + +namespace DNX.Extensions.Arrays; + +/// +/// Array Extensions +/// +public static class ArrayExtensions +{ + public static bool IsNullOrEmpty(this Array input) + { + return input == null || input.Length == 0; + } + + public static T[] PadLeft(this T[] input, int length) + { + var paddedArray = new T[length]; + var startIdx = length - input.Length; + if (length >= input.Length) + { + Array.Copy(input, 0, paddedArray, startIdx, input.Length); + } + else + { + Array.Copy(input, Math.Abs(startIdx), paddedArray, 0, length); + } + + return paddedArray; + } + + public static T[] ShiftLeft(this T[] input) + { + if (input == null) + { + return new T[0]; + } + + var shiftedArray = new T[input.Length]; + Array.Copy(input, 1, shiftedArray, 0, input.Length - 1); + + return shiftedArray; + } +} diff --git a/src/DNX.Extensions/Arrays/ByteArrayExtensions.cs b/src/DNX.Extensions/Arrays/ByteArrayExtensions.cs new file mode 100644 index 0000000..0204916 --- /dev/null +++ b/src/DNX.Extensions/Arrays/ByteArrayExtensions.cs @@ -0,0 +1,32 @@ +using System.Text; + +namespace DNX.Extensions.Arrays; + +/// +/// Byte Array Extensions +/// +public static class ByteArrayExtensions +{ + //private static string Base62CodingSpace = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + /// + /// Converts A byte array into an ASCII string + /// + /// The byte[] to turn into the string + public static string GetAsciiString(this byte[] input) + { + return Encoding.ASCII.GetString(input); + } + + /// + /// Converts A byte array into an hex string + /// + /// The byte[] to turn into the string + public static string ToHexString(this byte[] input) + { + var hex = new StringBuilder(input.Length * 2); + foreach (var b in input) + hex.AppendFormat("{0:x2}", b); + return hex.ToString(); + } +} diff --git a/src/DNX.Extensions/Assemblies/AssemblyExtensions.cs b/src/DNX.Extensions/Assemblies/AssemblyExtensions.cs new file mode 100644 index 0000000..17c3c06 --- /dev/null +++ b/src/DNX.Extensions/Assemblies/AssemblyExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Reflection; +using System.Resources; + +// ReSharper disable ConvertToUsingDeclaration + +namespace DNX.Extensions.Assemblies; + +/// +/// Assembly Extensions +/// +public static class AssemblyExtensions +{ + /// + /// Gets the embedded resource text. + /// + /// The assembly. + /// Name of the relative resource. + /// The name space. + /// + /// + public static string GetEmbeddedResourceText(this Assembly assembly, string relativeResourceName, string nameSpace = null) + { + try + { + nameSpace = string.IsNullOrWhiteSpace(nameSpace) + ? Path.GetFileNameWithoutExtension(assembly.Location) + : nameSpace; + + var resourceName = $"{nameSpace}.{relativeResourceName}"; + + using (var stream = assembly.GetManifestResourceStream(resourceName)) + { + using (var reader = new StreamReader(stream)) + { + var result = reader.ReadToEnd(); + + return result; + } + } + } + catch (Exception e) + { + throw new MissingManifestResourceException($"{relativeResourceName} not found", e); + } + } +} diff --git a/src/DNX.Extensions/Comparers/StringComparisonEqualityComparer.cs b/src/DNX.Extensions/Comparers/StringComparisonEqualityComparer.cs new file mode 100644 index 0000000..194ad7a --- /dev/null +++ b/src/DNX.Extensions/Comparers/StringComparisonEqualityComparer.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +#pragma warning disable IDE0290 // Use Primary Constructor + +namespace DNX.Extensions.Comparers; + +/// +/// String Equality Comparer based on +/// +/// +public class StringComparisonEqualityComparer : IEqualityComparer +{ + /// + /// The string comparison method + /// + public StringComparison StringComparisonMethod { get; } + + /// + /// Initializes a new instance of the class. + /// + public StringComparisonEqualityComparer() + : this(StringComparison.CurrentCulture) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The string comparison. + public StringComparisonEqualityComparer(StringComparison stringComparisonMethod) + { + StringComparisonMethod = stringComparisonMethod; + } + + /// + /// Determines whether the specified objects are equal. + /// + /// The first object of type T to compare. + /// The second object of type T to compare. + /// + /// true if the specified objects are equal; otherwise, false. + /// + /// + public bool Equals(string x, string y) + { + return string.Equals(x, y, StringComparisonMethod); + } + + /// + /// Returns a hash code for this instance. + /// + /// The object. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public int GetHashCode(string obj) + { + if (Equals(obj?.ToLowerInvariant(), obj?.ToUpperInvariant())) + return obj?.ToLowerInvariant().GetHashCode() ?? default; + + return obj.GetHashCode(); + } +} diff --git a/src/DNX.Extensions/Conversion/ConvertExtensions.cs b/src/DNX.Extensions/Conversion/ConvertExtensions.cs new file mode 100644 index 0000000..04f8112 --- /dev/null +++ b/src/DNX.Extensions/Conversion/ConvertExtensions.cs @@ -0,0 +1,103 @@ +using System; + +namespace DNX.Extensions.Conversion; + +/// +/// Extensions to simplify type conversion +/// +public static class ConvertExtensions +{ + /// + /// Converts to string, or default. + /// + /// The object. + /// The default value. + /// System.String. + public static string ToStringOrDefault(this object obj, string defaultValue = "") + { + return obj?.ToString() ?? defaultValue; + } + + /// + /// Converts to boolean. + /// + /// The text. + /// The default value. + /// true/false if can be converted, defaultValue otherwise. + public static bool ToBoolean(this string text, bool defaultValue = default) + { + return bool.TryParse(text, out var value) + ? value + : defaultValue; + } + + /// + /// Converts to int32. + /// + /// The text. + /// The default value. + /// System.Int32. + public static int ToInt32(this string text, int defaultValue = default) + { + return int.TryParse(text, out var value) + ? value + : defaultValue; + } + + /// + /// Converts to enum. + /// + /// + /// The text. + /// The default value. + /// T. + public static T ToEnum(this string text, T defaultValue = default) + where T : struct + { + return Enum.TryParse(text, true, out T value) + ? value + : defaultValue; + } + + /// + /// Converts to guid. + /// + /// The text. + /// Guid. + public static Guid ToGuid(this string text) + { + return text.ToGuid(Guid.Empty); + } + + /// + /// Converts to guid. + /// + /// The text. + /// The default value. + /// Guid. + public static Guid ToGuid(this string text, Guid defaultValue) + { + return Guid.TryParse(text, out var result) + ? result + : defaultValue; + } + + /// + /// Converts to the specified type. + /// + /// + /// The object. + /// The default value. + /// T. + public static T To(this object obj, T defaultValue = default) + { + try + { + return (T)obj ?? defaultValue; + } + catch + { + return defaultValue; + } + } +} diff --git a/src/DNX.Extensions/Conversion/GuidExtensions.cs b/src/DNX.Extensions/Conversion/GuidExtensions.cs new file mode 100644 index 0000000..283406d --- /dev/null +++ b/src/DNX.Extensions/Conversion/GuidExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace DNX.Extensions.Conversion; + +/// +/// Extensions for working with Guids +/// +public static class GuidExtensions +{ + /// + /// Convert any text item to a guid. + /// + /// + /// The result is deterministic in that each text item will always generate the same result + /// NOTE: If the text item is actually a Guid, it will NOT be parsed directly to a Guid + /// + /// + /// + /// A + /// + public static Guid ToDeterministicGuid(this string input) + { + input ??= string.Empty; + + //use MD5 hash to get a 16-byte hash of the string: + using var provider = new MD5CryptoServiceProvider(); + + var inputBytes = Encoding.Default.GetBytes(input); + var hashBytes = provider.ComputeHash(inputBytes); + + //generate a guid from the hash: + var hashGuid = new Guid(hashBytes); + + return hashGuid; + } +} diff --git a/src/DNX.Extensions/DateTimes/DateTimeExtensions.cs b/src/DNX.Extensions/DateTimes/DateTimeExtensions.cs new file mode 100644 index 0000000..15370b8 --- /dev/null +++ b/src/DNX.Extensions/DateTimes/DateTimeExtensions.cs @@ -0,0 +1,163 @@ +using System; + +namespace DNX.Extensions.DateTimes; + +/// +/// Date Time Extensions +/// +public static class DateTimeExtensions +{ + /// + /// Sets the year. + /// + /// The date time. + /// The year. + /// + public static DateTime SetYear(this DateTime dateTime, int year) + { + return dateTime.Year == year + ? dateTime + : dateTime.AddYears(year - dateTime.Year); + } + + /// + /// Sets the month. + /// + /// The date time. + /// The month. + /// if set to true [maintain year]. + /// if set to true [maintain day]. + /// + public static DateTime SetMonth(this DateTime dateTime, int month, bool maintainYear = true, bool maintainDay = true) + { + if (dateTime.Month == month) + return dateTime; + + var year = dateTime.Year; + var day = dateTime.Day; + + var result = dateTime.AddMonths(month - dateTime.Month); + + if (maintainDay) + result = result.SetDay(day, false, false); + if (maintainYear) + result = result.SetYear(year); + + return result; + } + + /// + /// Sets the day. + /// + /// The date time. + /// The day. + /// if set to true [maintain year]. + /// if set to true [maintain month]. + /// + public static DateTime SetDay(this DateTime dateTime, int day, bool maintainYear = true, bool maintainMonth = true) + { + if (dateTime.Day == day) + return dateTime; + + var year = dateTime.Year; + var month = dateTime.Month; + + var result = dateTime.AddDays(day - dateTime.Day); + + if (maintainMonth) + result = result.SetMonth(month, false, false); + if (maintainYear) + result = result.SetYear(year); + + return result; + } + + /// + /// Resets the hours on a DateTime + /// + /// The date time. + /// + public static DateTime ResetHours(this DateTime dateTime) + { + return dateTime.Subtract(TimeSpan.FromHours(dateTime.Hour)); + } + + /// + /// Resets the minutes on a DateTime + /// + /// The date time. + /// + public static DateTime ResetMinutes(this DateTime dateTime) + { + return dateTime.Subtract(TimeSpan.FromMinutes(dateTime.Minute)); + } + + /// + /// Resets the seconds on a DateTime + /// + /// The date time. + /// + public static DateTime ResetSeconds(this DateTime dateTime) + { + return dateTime.Subtract(TimeSpan.FromSeconds(dateTime.Second)); + } + + /// + /// Resets the milliseconds on a DateTime + /// + /// The date time. + /// + public static DateTime ResetMilliseconds(this DateTime dateTime) + { + return dateTime.Subtract(TimeSpan.FromMilliseconds(dateTime.Millisecond)); + } + + /// + /// Sets the hours on a DateTime + /// + /// The date time. + /// The hours. + /// + public static DateTime SetHours(this DateTime dateTime, int hours) + { + return dateTime.ResetHours() + .AddHours(hours); + } + + /// + /// Sets the minutes on a DateTime + /// + /// The date time. + /// The minutes. + /// + public static DateTime SetMinutes(this DateTime dateTime, int minutes) + { + return dateTime.ResetMinutes() + .AddMinutes(minutes); + } + + /// + /// Sets the seconds on a DateTime + /// + /// The date time. + /// The seconds. + /// + public static DateTime SetSeconds(this DateTime dateTime, int seconds) + { + return dateTime.ResetSeconds() + .AddSeconds(seconds); + } + + /// + /// Sets the milliseconds on a DateTime + /// + /// The date time. + /// The milliseconds. + /// Precision on a means this only works for values up to 999 + /// + public static DateTime SetMilliseconds(this DateTime dateTime, double milliseconds) + { + return dateTime.ResetMilliseconds() + .AddMilliseconds(milliseconds); + } +} diff --git a/src/DNX.Extensions/Dictionaries/DictionaryExtensions.cs b/src/DNX.Extensions/Dictionaries/DictionaryExtensions.cs new file mode 100644 index 0000000..c57c6cb --- /dev/null +++ b/src/DNX.Extensions/Dictionaries/DictionaryExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DNX.Extensions.Dictionaries; + +/// +/// Dictionary Extensions +/// +public static class DictionaryExtensions +{ + /// + /// Gets the specified key value + /// + /// The type of the k. + /// The type of the v. + /// The dictionary. + /// The key. + /// The default value. + /// + public static TV Get(this IDictionary dictionary, TK key, TV defaultValue = default) + { + return dictionary != null && key != null && dictionary.TryGetValue(key, out var value) + ? value + : defaultValue; + } + + /// + /// Converts a string to Dictionary<string, string> + /// + /// The text. + /// The element separator. + /// The value separator. + /// + public static IDictionary ToStringDictionary(this string text, string elementSeparator = "|", string valueSeparator = "=") + { + var dictionary = (text ?? string.Empty) + .Split(elementSeparator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries) + .ToDictionary( + x => x.Split(valueSeparator.ToCharArray()).FirstOrDefault(), + x => string.Join(valueSeparator, x.Split(valueSeparator.ToCharArray()).Skip(1)) + ); + + return dictionary; + } + + /// + /// Converts a string to Dictionary<string, object> + /// + /// The text. + /// The element separator. + /// The value separator. + /// + public static IDictionary ToStringObjectDictionary(this string text, string elementSeparator = "|", string valueSeparator = "=") + { + var dictionary = text + .ToStringDictionary(elementSeparator, valueSeparator) + .ToDictionary( + x => x.Key, + x => (object)x.Value + ); + + return dictionary; + } +} diff --git a/src/DNX.Extensions/Enumerations/EnumerableExtensions.cs b/src/DNX.Extensions/Enumerations/EnumerableExtensions.cs deleted file mode 100644 index 04be678..0000000 --- a/src/DNX.Extensions/Enumerations/EnumerableExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace DNX.Extensions.Enumerations; - -public static class EnumerableExtensions -{ - /// - /// Determines whether the specified enumerable has any elements and is not null - /// - /// - /// The enumerable. - /// true if the specified enumerable has any elements; otherwise, false. - /// Also available as an extension method - public static bool HasAny(this IEnumerable enumerable) - { - return enumerable != null && enumerable.Any(); - } - - /// - /// Determines whether the specified enumerable has any elements that match the predicate and is not null - /// - /// - /// The enumerable. - /// The predicate. - /// true if the specified predicate has any elements that match the predicate; otherwise, false. - /// Also available as an extension method - public static bool HasAny(this IEnumerable enumerable, Func predicate) - { - return enumerable != null && enumerable.Any(predicate); - } -} diff --git a/src/DNX.Extensions/Enums/EnumExtensions.cs b/src/DNX.Extensions/Enums/EnumExtensions.cs new file mode 100644 index 0000000..149107d --- /dev/null +++ b/src/DNX.Extensions/Enums/EnumExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.ComponentModel; + +namespace DNX.Extensions.Enums; + +/// +/// Enum Extensions +/// +public static class EnumExtensions +{ + /// + /// Gets the attribute. + /// + /// + /// The en. + /// + public static T GetAttribute(this Enum en) + { + var type = en.GetType(); + + var memInfo = type.GetMember(en.ToString()); + + if (memInfo.Length > 0) + { + var attrs = memInfo[0].GetCustomAttributes(typeof(T), false); + + if (attrs.Length > 0) + { + return (T)attrs[0]; + } + } + + return default; + } + + /// + /// Retrieve the description on the enum, e.g. + /// [Description("Bright Pink")] + /// BrightPink = 2, + /// Then when you pass in the enum, it will retrieve the description + /// + /// The Enumeration + /// A string representing the friendly name + public static string GetDescription(this Enum en) + { + var attr = en.GetAttribute(); + + return attr == null + ? en.ToString() + : attr.Description; + } +} diff --git a/src/DNX.Extensions/Execution/RunSafely.cs b/src/DNX.Extensions/Execution/RunSafely.cs new file mode 100644 index 0000000..114b979 --- /dev/null +++ b/src/DNX.Extensions/Execution/RunSafely.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; + +namespace DNX.Extensions.Execution; + +/// +/// Execute code without having to implement try...catch +/// +public class RunSafely +{ + /// + /// Executes the specified action. + /// + /// The action. + /// The exception handler. + public static void Execute(Action action, Action exceptionHandler = null) + { + try + { + action.Invoke(); + } + catch (Exception ex) + { + Execute(() => exceptionHandler?.Invoke(ex)); + } + } + + /// + /// Executes the specified function. + /// + /// + /// The function. + /// The exception handler. + /// T. + public static T Execute(Func func, Action exceptionHandler = null) + { + return Execute(func, default, exceptionHandler); + } + + /// + /// Executes the specified function. + /// + /// + /// The function. + /// The default value. + /// The exception handler. + /// T. + public static T Execute(Func func, T defaultValue, Action exceptionHandler = null) + { + try + { + return func.Invoke(); + } + catch (Exception ex) + { + Execute(() => exceptionHandler?.Invoke(ex)); + + return defaultValue; + } + } + + /// + /// execute as an asynchronous operation. + /// + /// The task. + /// The exception handler. + public static async Task ExecuteAsync(Task task, Action exceptionHandler = null) + { + try + { + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + Execute(() => exceptionHandler?.Invoke(ex)); + } + } + + /// + /// execute as an asynchronous operation. + /// + /// + /// The task. + /// The exception handler. + /// Task<T>. + public static async Task ExecuteAsync(Task task, Action exceptionHandler = null) + { + return await ExecuteAsync(task, default, exceptionHandler); + } + + /// + /// execute as an asynchronous operation. + /// + /// + /// The task. + /// The default value. + /// The exception handler. + /// Task<T>. + public static async Task ExecuteAsync(Task task, T defaultValue, Action exceptionHandler = null) + { + try + { + return await task.ConfigureAwait(false); + } + catch (Exception ex) + { + Execute(() => exceptionHandler?.Invoke(ex)); + + return defaultValue; + } + } +} diff --git a/src/DNX.Extensions/IO/DirectoryInfoExtensions.cs b/src/DNX.Extensions/IO/DirectoryInfoExtensions.cs new file mode 100644 index 0000000..ce2b1e5 --- /dev/null +++ b/src/DNX.Extensions/IO/DirectoryInfoExtensions.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using DNX.Extensions.Strings; + +namespace DNX.Extensions.IO; + +/// +/// DirectoryInfo Extensions. +/// +public static class DirectoryInfoExtensions +{ + /// + /// Finds files based on pattern + /// + /// The directory information. + /// The pattern. + /// if set to true [recurse directories]. + /// IEnumerable<FileInfo>. + public static IEnumerable FindFiles(this DirectoryInfo directoryInfo, string pattern, bool recurseDirectories = true) + { + var searchOption = recurseDirectories + ? SearchOption.AllDirectories + : SearchOption.TopDirectoryOnly; + + var files = directoryInfo.Exists + ? directoryInfo.EnumerateFiles(pattern, searchOption) + : []; + + return files; + } + + /// + /// Finds files based on pattern + /// + /// The directory information. + /// The patterns. + /// if set to true [recurse directories]. + /// IEnumerable<FileInfo>. + public static IEnumerable FindFiles(this DirectoryInfo directoryInfo, string[] patterns, bool recurseDirectories = true) + { + var fileInfos = patterns + .SelectMany(p => directoryInfo.FindFiles(p, recurseDirectories)); + + return fileInfos; + } + + /// + /// Finds directories based on pattern + /// + /// The directory information. + /// The pattern. + /// if set to true [recurse directories]. + /// IEnumerable<DirectoryInfo>. + public static IEnumerable FindDirectories(this DirectoryInfo directoryInfo, string pattern, bool recurseDirectories = true) + { + var searchOption = recurseDirectories + ? SearchOption.AllDirectories + : SearchOption.TopDirectoryOnly; + + var directoryInfos = directoryInfo.Exists + ? directoryInfo.EnumerateDirectories(pattern, searchOption) + : []; + + return directoryInfos; + } + + /// + /// Finds directories based on pattern + /// + /// The directory information. + /// The patterns. + /// if set to true [recurse directories]. + /// IEnumerable<DirectoryInfo>. + public static IEnumerable FindDirectories(this DirectoryInfo directoryInfo, string[] patterns, bool recurseDirectories = true) + { + var directoryInfos = patterns + .SelectMany(p => directoryInfo.FindDirectories(p, recurseDirectories)); + + return directoryInfos; + } + + /// + /// Gets the relative file path. + /// + /// The directory information. + /// The owning directory information. + /// System.String. + public static string GetRelativePath(this DirectoryInfo directoryInfo, DirectoryInfo owningDirectoryInfo) + { + var relativePath = owningDirectoryInfo == null || directoryInfo == null + ? null + : GetRelativePath(directoryInfo.FullName, owningDirectoryInfo.FullName) + .RemoveStartsWith($".{Path.DirectorySeparatorChar}"); + + if (relativePath == ".") + { + relativePath = string.Empty; + } + + return relativePath; + } + /// + /// Returns a relative path string from a full path based on a base path + /// provided. + /// + /// The path to convert. Can be either a file or a directory + /// The base path on which relative processing is based. Should be a directory. + /// + /// String of the relative path. + /// + /// Examples of returned values: + /// test.txt, ..\test.txt, ..\..\..\test.txt, ., .., subdir\test.txt + /// + /// + /// From : http://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path + /// + private static string GetRelativePath(string fullPath, string basePath) + { +#if NETSTANDARD2_1_OR_GREATER + return Path.GetRelativePath(basePath, fullPath); +#else + // Require trailing path separator for path + fullPath = fullPath.EnsureEndsWith(Path.DirectorySeparatorChar.ToString()); + basePath = basePath.EnsureEndsWith(Path.DirectorySeparatorChar.ToString()); + + var baseUri = new Uri(basePath); + var fullUri = new Uri(fullPath); + + var relativeUri = baseUri.MakeRelativeUri(fullUri); + + // Uri's use forward slashes so convert back to OS slashes + return relativeUri.ToString() + .Replace("/", Path.DirectorySeparatorChar.ToString()) + .RemoveEndsWith(Path.DirectorySeparatorChar.ToString()); +#endif + } +} diff --git a/src/DNX.Extensions/IO/FileInfoExtensions.cs b/src/DNX.Extensions/IO/FileInfoExtensions.cs new file mode 100644 index 0000000..0eb34ce --- /dev/null +++ b/src/DNX.Extensions/IO/FileInfoExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; + +namespace DNX.Extensions.IO; + +/// +/// FileInfo Extensions. +/// +public static class FileInfoExtensions +{ + /// + /// Gets the name of the relative file. + /// + /// The file information. + /// The directory information. + /// System.String. + public static string GetRelativeFileName(this FileInfo fileInfo, DirectoryInfo directoryInfo) + { + return Path.Combine(fileInfo.GetRelativeFilePath(directoryInfo), fileInfo.Name); + } + + /// + /// Gets the relative file path. + /// + /// The file information. + /// The directory information. + /// System.String. + public static string GetRelativeFilePath(this FileInfo fileInfo, DirectoryInfo directoryInfo) + { + return fileInfo.Directory.GetRelativePath(directoryInfo); + } + + /// + /// Gets the friendly size of the file. + /// + /// The file information. + /// System.String. + public static string GetFriendlyFileSize(FileInfo fileInfo) + { + return GetFriendlyFileSize(fileInfo?.Length ?? 0); + } + + /// + /// Gets the friendly size of the file. + /// + /// Size of the file. + /// System.String. + /// > + /// + /// Based on: https://stackoverflow.com/questions/281640/how-do-i-get-a-human-readable-file-size-in-bytes-abbreviation-using-net + /// + public static string GetFriendlyFileSize(long fileSize) + { + string[] suffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB + + var num = 0d; + var place = 0; + + if (fileSize > 0) + { + var bytes = Math.Abs(fileSize); + + place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); + num = Math.Round(bytes / Math.Pow(1024, place), 1); + } + + return $"{Math.Sign(fileSize) * num}{suffixes[place]}"; + } +} diff --git a/src/DNX.Extensions/Linq/EnumerableExtensions.cs b/src/DNX.Extensions/Linq/EnumerableExtensions.cs new file mode 100644 index 0000000..ba40738 --- /dev/null +++ b/src/DNX.Extensions/Linq/EnumerableExtensions.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// ReSharper disable PossibleMultipleEnumeration + +namespace DNX.Extensions.Linq; + +/// +/// Enumerable Extensions +/// +public static class EnumerableExtensions +{ + private static readonly Random Randomizer = new(); + + /// + /// Determines whether the specified enumerable has any elements and is not null + /// + /// + /// The enumerable. + /// true if the specified enumerable has any elements; otherwise, false. + /// Also available as an extension method + public static bool HasAny(this IEnumerable enumerable) + { + return enumerable != null && enumerable.Any(); + } + + /// + /// Determines whether the specified enumerable has any elements that match the predicate and is not null + /// + /// + /// The enumerable. + /// The predicate. + /// true if the specified predicate has any elements that match the predicate; otherwise, false. + /// Also available as an extension method + public static bool HasAny(this IEnumerable enumerable, Func predicate) + { + return enumerable != null && enumerable.Any(predicate); + } + + /// + /// Determine if enumerable contains any of the specified candidates + /// + /// + /// The value. + /// The candidates. + /// + /// true if input is one of the specified candidates; otherwise, false. + /// + public static bool IsOneOf(this T value, params T[] candidates) + => value.IsOneOf(candidates?.ToList()); + + /// + /// Determine if enumerable contains any of the specified candidates, using a Comparer + /// + /// + /// The value. + /// The comparer. + /// The candidates. + /// + /// true if input is one of the specified candidates; otherwise, false. + /// + public static bool IsOneOf(this T value, IEqualityComparer comparer, params T[] candidates) + => value.IsOneOf(candidates?.ToList(), comparer); + + /// + /// Determines whether input is one of the specified candidates. + /// + /// + /// The input. + /// The candidates. + /// + /// true if input is one of the specified candidates; otherwise, false. + /// + public static bool IsOneOf(this T input, IEnumerable candidates) + { + return candidates != null && candidates.Any() && candidates.Contains(input); + } + + /// + /// Determines whether input is one of the specified candidates, according to Comparison Method + /// + /// + /// The input. + /// The candidates. + /// The comparer. + /// + /// true if input is one of the specified candidates; otherwise, false. + /// + public static bool IsOneOf(this T input, IEnumerable candidates, IEqualityComparer comparer) + { + return candidates != null && candidates.Any() && candidates.Contains(input, comparer); + } + + /// + /// Gets the random item. + /// + /// + /// The items. + /// The randomizer to use (optional) + /// + public static T GetRandomItem(this IEnumerable items, Random randomizer = null) + { + // ReSharper disable PossibleMultipleEnumeration + if (!items.HasAny()) + return default; + + var list = items.ToArray(); + // ReSharper restore PossibleMultipleEnumeration + + var index = (randomizer ?? Randomizer).Next(list.Length); + + return list[index]; + } + + /// + /// Gets the item at an index position, or default + /// + /// + /// The items. + /// The index. + /// + public static T GetAt(this IList items, int index) + { + return items != null && index >= 0 && index < items.Count + ? items[index] + : default; + } +} diff --git a/src/DNX.Extensions/Properties/launchSettings.json b/src/DNX.Extensions/Properties/launchSettings.json new file mode 100644 index 0000000..50794bf --- /dev/null +++ b/src/DNX.Extensions/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "DNX.Extensions": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/src/DNX.Extensions/Reflection/ReflectionExtensions.cs b/src/DNX.Extensions/Reflection/ReflectionExtensions.cs new file mode 100644 index 0000000..5efaa5f --- /dev/null +++ b/src/DNX.Extensions/Reflection/ReflectionExtensions.cs @@ -0,0 +1,48 @@ +using System.Reflection; + +namespace DNX.Extensions.Reflection; + +/// +/// Extensions for simplifying Reflection tasks +/// +public static class ReflectionExtensions +{ + /// + /// Gets the property value of an instance by name + /// + /// + /// The instance. + /// Name of the property. + /// The flags. + /// The default value. + /// System.Object. + public static object GetPropertyValueByName(this T instance, string propertyName, BindingFlags flags, object defaultValue = default) + { + var pi = typeof(T).GetProperty(propertyName, flags); + if (pi == null) + { + return defaultValue; + } + + var allowNonPublic = flags.HasFlag(BindingFlags.NonPublic) || !flags.HasFlag(BindingFlags.Public); + var getter = pi.GetGetMethod(allowNonPublic); + + var value = getter?.Invoke(instance, null) + ?? defaultValue; + + return value; + } + + /// + /// Gets a private property value by name + /// + /// + /// The instance. + /// Name of the property. + /// The default value. + /// System.Object. + public static object GetPrivatePropertyValue(this T instance, string propertyName, object defaultValue = default) + { + return instance.GetPropertyValueByName(propertyName, BindingFlags.Instance | BindingFlags.NonPublic, defaultValue); + } +} diff --git a/src/DNX.Extensions/Strings/ArgumentParserExtensions.cs b/src/DNX.Extensions/Strings/ArgumentParserExtensions.cs new file mode 100644 index 0000000..bb5d903 --- /dev/null +++ b/src/DNX.Extensions/Strings/ArgumentParserExtensions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace DNX.Extensions.Strings; + +/// +/// Extensions for textual argument parsing +/// +public static class ArgumentParserExtensions +{ + /// + /// Parses a raw command line string into an array of parts + /// + /// The text. + /// IList<System.String>. + /// + /// See: https://stackoverflow.com/questions/14655023/split-a-string-that-has-white-spaces-unless-they-are-enclosed-within-quotes + /// + public static IList ParseArguments(this string text) + { + var parts = Regex.Matches(text, @"[\""].+?[\""]|[^ ]+") + .Cast() + .Select(m => m.Value.Trim("\"".ToCharArray())) + .ToList(); + + return parts; + } +} diff --git a/src/DNX.Extensions/Strings/StringExtensions.cs b/src/DNX.Extensions/Strings/StringExtensions.cs index 38e4145..5ce6710 100644 --- a/src/DNX.Extensions/Strings/StringExtensions.cs +++ b/src/DNX.Extensions/Strings/StringExtensions.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; -using DNX.Extensions.Enumerations; +using DNX.Extensions.Linq; // ReSharper disable InconsistentNaming diff --git a/tests/DNX.Extensions.Tests/Arrays/ArrayExtensionsTests.cs b/tests/DNX.Extensions.Tests/Arrays/ArrayExtensionsTests.cs new file mode 100644 index 0000000..caf1e79 --- /dev/null +++ b/tests/DNX.Extensions.Tests/Arrays/ArrayExtensionsTests.cs @@ -0,0 +1,86 @@ +using DNX.Extensions.Arrays; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +#pragma warning disable IDE0290 + +namespace DNX.Extensions.Tests.Arrays +{ + public class ArrayExtensionsTests(ITestOutputHelper outputHelper) + { + [Theory] + [InlineData("2,3,4", false)] + [InlineData("", true)] + [InlineData(null, true)] + public void Test_IsNullOrEmpty(string byteArray, bool isEmpty) + { + var bytes = byteArray?.Split(",", StringSplitOptions.RemoveEmptyEntries) + .Select(x => Convert.ToByte(x)) + .ToArray(); + + bytes.IsNullOrEmpty().Should().Be(isEmpty); + } + + [Fact] + public void Test_PadLeft_when_greater_than_existing_array_length() + { + var values = "1,2,3,4,5,6".Split(",").ToArray(); + + var result = values.PadLeft(8); + + values.Length.Should().Be(6); + result.Length.Should().Be(8); + + for (var i = 1; i <= result.Length; ++i) + { + outputHelper.WriteLine($"Index: {i}"); + result[^i].Should().Be(i > values.Length ? default : values[^i]); + } + } + + [Fact] + public void Test_PadLeft_when_less_than_existing_array_length() + { + var values = "1,2,3,4,5,6".Split(",").ToArray(); + + var result = values.PadLeft(4); + + values.Length.Should().Be(6); + result.Length.Should().Be(4); + + for (var i = 1; i <= result.Length; ++i) + { + outputHelper.WriteLine($"Index: {i}"); + result[^i].Should().Be(values[^i]); + } + } + + [Fact] + public void Test_ShiftLeft_populated_array() + { + var values = "1,2,3,4,5,6".Split(",").ToArray(); + + var result = values.ShiftLeft(); + + result.Length.Should().Be(values.Length); + + for (var i = 0; i < result.Length; ++i) + { + outputHelper.WriteLine($"Index: {i}"); + result[i].Should().Be(i >= values.Length - 1 ? default : values[i + 1]); + } + } + + [Fact] + public void Test_ShiftLeft_null_array() + { + string[] values = null; + + var result = values.ShiftLeft(); + + result.Should().NotBeNull(); + result.Length.Should().Be(0); + } + } +} diff --git a/tests/DNX.Extensions.Tests/Arrays/ByteArrayExtensionsTests.cs b/tests/DNX.Extensions.Tests/Arrays/ByteArrayExtensionsTests.cs new file mode 100644 index 0000000..985bcf5 --- /dev/null +++ b/tests/DNX.Extensions.Tests/Arrays/ByteArrayExtensionsTests.cs @@ -0,0 +1,40 @@ +using DNX.Extensions.Arrays; +using FluentAssertions; +using Xunit; + +namespace DNX.Extensions.Tests.Arrays +{ + public class ByteArrayExtensionsTests + { + [Theory] + [InlineData("65,66,67,68,69,70", "ABCDEF")] + [InlineData("97,98,99,100,101,102", "abcdef")] + [InlineData("", "")] + [InlineData(null, "")] + public void Test_GetAsciiString(string byteText, string expectedResult) + { + var bytes = byteText == null + ? [] + : byteText.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToByte(x)).ToArray(); + + var result = bytes.GetAsciiString(); + + result.Should().Be(expectedResult); + } + + [Theory] + [InlineData("1,2,3,4,5,6", "010203040506")] + [InlineData("", "")] + [InlineData(null, "")] + public void Test_ToHexString(string byteText, string expectedResult) + { + var bytes = byteText == null + ? [] + : byteText.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToByte(x)).ToArray(); + + var result = bytes.ToHexString(); + + result.Should().Be(expectedResult); + } + } +} diff --git a/tests/DNX.Extensions.Tests/Assemblies/AssemblyExtensionsTests.cs b/tests/DNX.Extensions.Tests/Assemblies/AssemblyExtensionsTests.cs new file mode 100644 index 0000000..49111ab --- /dev/null +++ b/tests/DNX.Extensions.Tests/Assemblies/AssemblyExtensionsTests.cs @@ -0,0 +1,61 @@ +using System.Reflection; +using System.Resources; +using DNX.Extensions.Assemblies; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace DNX.Extensions.Tests.Assemblies +{ + public class AssemblyExtensionsTests(ITestOutputHelper testOutputHelper) + { + [Fact] + public void GetEmbeddedResourceText_can_read_resource_successfully() + { + // Arrange + var name = "TestData.SampleData.json"; + + // Act + var result = Assembly.GetExecutingAssembly().GetEmbeddedResourceText(name); + + testOutputHelper.WriteLine("Result: {0}", result); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public void GetEmbeddedResourceText_throws_on_unknown_resource_name() + { + // Arrange + var name = $"{Guid.NewGuid()}.json"; + + // Act + var ex = Assert.Throws( + () => Assembly.GetExecutingAssembly().GetEmbeddedResourceText(name) + ); + + testOutputHelper.WriteLine("Exception Message: {0}", ex?.Message); + + // Assert + ex.Should().NotBeNull(); + ex.Message.Should().Contain(name); + } + + [Fact] + public void GetEmbeddedResourceText_can_read_resource_with_specific_namespace_successfully() + { + // Arrange + var name = "SampleData.json"; + var nameSpace = $"DNX.Extensions.Tests.TestData"; + + // Act + var result = Assembly.GetExecutingAssembly().GetEmbeddedResourceText(name, nameSpace); + + testOutputHelper.WriteLine("Result: {0}", result); + + // Assert + result.Should().NotBeNull(); + } + } +} diff --git a/tests/DNX.Extensions.Tests/Comparers/StringComparisonEqualityComparerTests.cs b/tests/DNX.Extensions.Tests/Comparers/StringComparisonEqualityComparerTests.cs new file mode 100644 index 0000000..f1161ba --- /dev/null +++ b/tests/DNX.Extensions.Tests/Comparers/StringComparisonEqualityComparerTests.cs @@ -0,0 +1,105 @@ +using DNX.Extensions.Comparers; +using FluentAssertions; +using Xunit; + +namespace DNX.Extensions.Tests.Comparers +{ + public class StringComparisonEqualityComparerTests + { + private StringComparison _comparisonMethod = StringComparison.CurrentCultureIgnoreCase; + private StringComparisonEqualityComparer Sut => new(_comparisonMethod); + + [Fact] + public void DefaultConstructor_works_as_expected() + { + var sut = new StringComparisonEqualityComparer(); + + sut.StringComparisonMethod.Should().Be(StringComparison.CurrentCulture); + } + + [Theory] + [MemberData(nameof(StringComparisonValues_Data))] + public void Constructor_for_StringComparison_works_as_expected(StringComparison stringComparison) + { + var sut = new StringComparisonEqualityComparer(stringComparison); + + sut.StringComparisonMethod.Should().Be(stringComparison); + } + + [Theory] + [MemberData(nameof(Equals_Data))] + public void Equals_compares_as_expected(string x, string y, StringComparison stringComparison, bool expectedResult) + { + // Arrange + _comparisonMethod = stringComparison; + + // Act + var result = Sut.Equals(x, y); + + // Assert + result.Should().Be(expectedResult); + } + + [Theory] + [MemberData(nameof(Equals_Data))] + public void GetHashCode_compares_as_expected(string x, string y, StringComparison stringComparison, bool expectedResult) + { + // Arrange + _comparisonMethod = stringComparison; + + // Act + var resultX = Sut.GetHashCode(x); + var resultY = Sut.GetHashCode(y); + + // Assert + (resultX == resultY).Should().Be(expectedResult); + } + + #region TestData + + public static TheoryData StringComparisonValues_Data() + { + return new TheoryData( + Enum.GetValues(typeof(StringComparison)) + .Cast() + ); + } + + public static TheoryData Equals_Data() + { + return new TheoryData + { + { null, null, StringComparison.CurrentCulture, true }, + { "", "", StringComparison.CurrentCulture, true }, + { "ClearBank", "", StringComparison.CurrentCulture, false }, + { "ClearBank", "", StringComparison.CurrentCulture, false }, + { "", "ClearBank", StringComparison.CurrentCulture, false }, + { "ClearBank", null, StringComparison.CurrentCulture, false }, + { null, "ClearBank", StringComparison.CurrentCulture, false }, + + { "Clear", "Bank", StringComparison.CurrentCulture, false }, + { "Clear", "Bank", StringComparison.CurrentCultureIgnoreCase, false }, + { "Clear", "Bank", StringComparison.InvariantCulture, false }, + { "Clear", "Bank", StringComparison.InvariantCultureIgnoreCase, false }, + { "Clear", "Bank", StringComparison.Ordinal, false }, + { "Clear", "Bank", StringComparison.OrdinalIgnoreCase, false }, + + { "ClearBank", "ClearBank", StringComparison.CurrentCulture, true }, + { "ClearBank", "ClearBank", StringComparison.CurrentCultureIgnoreCase, true }, + { "ClearBank", "ClearBank", StringComparison.InvariantCulture, true }, + { "ClearBank", "ClearBank", StringComparison.InvariantCultureIgnoreCase, true }, + { "ClearBank", "ClearBank", StringComparison.Ordinal, true }, + { "ClearBank", "ClearBank", StringComparison.OrdinalIgnoreCase, true }, + + { "ClearBank", "CLEARBANK", StringComparison.CurrentCulture, false }, + { "ClearBank", "CLEARBANK", StringComparison.CurrentCultureIgnoreCase, true }, + { "ClearBank", "CLEARBANK", StringComparison.InvariantCulture, false }, + { "ClearBank", "CLEARBANK", StringComparison.InvariantCultureIgnoreCase, true }, + { "ClearBank", "CLEARBANK", StringComparison.Ordinal, false }, + { "ClearBank", "CLEARBANK", StringComparison.OrdinalIgnoreCase, true }, + }; + } + + #endregion + } +} diff --git a/tests/DNX.Extensions.Tests/Configuration/EnvironmentConfig.cs b/tests/DNX.Extensions.Tests/Configuration/EnvironmentConfig.cs new file mode 100644 index 0000000..ebd8144 --- /dev/null +++ b/tests/DNX.Extensions.Tests/Configuration/EnvironmentConfig.cs @@ -0,0 +1,9 @@ +using DNX.Extensions.Linq; + +namespace DNX.Extensions.Tests.Configuration; +public class EnvironmentConfig +{ + public static bool IsLinuxStyleFileSystem => Environment.OSVersion.Platform.IsOneOf(PlatformID.Unix, PlatformID.MacOSX); + + public static bool IsWindowsStyleFileSystem => Environment.OSVersion.Platform.ToString().StartsWith("Win"); +} diff --git a/tests/DNX.Extensions.Tests/Conversion/ConvertExtensionsTest.cs b/tests/DNX.Extensions.Tests/Conversion/ConvertExtensionsTest.cs new file mode 100644 index 0000000..aa5600c --- /dev/null +++ b/tests/DNX.Extensions.Tests/Conversion/ConvertExtensionsTest.cs @@ -0,0 +1,229 @@ +using DNX.Extensions.Conversion; +using FluentAssertions; +using Xunit; + +namespace DNX.Extensions.Tests.Conversion +{ + public class ConvertExtensionsTests + { + public enum Numbers + { + Zero = 0, + One, + Two, + Three, + Four, + Five + } + + public class TestClass1 { } + public class TestClass2 : TestClass1 { } + public class TestClass3 { } + + public class ToStringOrDefault + { + [Theory] + [InlineData(1, "1")] + [InlineData(12.34, "12.34")] + [InlineData(true, "True")] + [InlineData((string)null, "")] + public void ToStringOrDefault_without_override_can_convert_successfully(object instance, string expected) + { + var result = instance.ToStringOrDefault(); + + result.Should().Be(expected); + } + + [Theory] + [InlineData(1, "bob", "1")] + [InlineData(12.34, "bob", "12.34")] + [InlineData(true, "bob", "True")] + [InlineData((string)null, "bob", "bob")] + public void ToStringOrDefault_with_override_can_convert_successfully(object instance, string defaultValue, string expected) + { + var result = instance.ToStringOrDefault(defaultValue); + + result.Should().Be(expected); + } + } + + public class ToBoolean + { + [Theory] + [InlineData("true", true)] + [InlineData("TRUE", true)] + [InlineData("TrUe", true)] + [InlineData("false", false)] + [InlineData("FALSE", false)] + [InlineData("FaLsE", false)] + [InlineData("NotABoolean", false)] + public void ToBoolean_without_override_can_convert_successfully(string text, bool expected) + { + var result = text.ToBoolean(); + + result.Should().Be(expected); + } + + [Theory] + [InlineData("true", false, true)] + [InlineData("TRUE", false, true)] + [InlineData("TrUe", false, true)] + [InlineData("false", true, false)] + [InlineData("FALSE", true, false)] + [InlineData("FaLsE", true, false)] + [InlineData("NotABoolean", false, false)] + [InlineData("NotABoolean", true, true)] + public void ToBoolean_with_override_can_convert_successfully(string text, bool defaultValue, bool expected) + { + var result = text.ToBoolean(defaultValue); + + result.Should().Be(expected); + } + } + + public class ToInt32 + { + [Theory] + [InlineData("160", 160)] + [InlineData("0", 0)] + [InlineData("-1", -1)] + [InlineData("2147483647", 2147483647)] + [InlineData("2147483648", 0)] + [InlineData("NotAnInt32", 0)] + public void ToInt32_without_override_can_convert_successfully(string text, int expected) + { + var result = text.ToInt32(); + + result.Should().Be(expected); + } + + [Theory] + [InlineData("160", 42, 160)] + [InlineData("0", 57, 0)] + [InlineData("-1", 5, -1)] + [InlineData("2147483647", 12345, 2147483647)] + [InlineData("2147483648", 12345, 12345)] + [InlineData("NotAnInt32", 0, 0)] + [InlineData("NotAnInt32", 222, 222)] + public void ToInt32_with_override_can_convert_successfully(string text, int defaultValue, int expected) + { + var result = text.ToInt32(defaultValue); + + result.Should().Be(expected); + } + } + + public class ToEnum + { + [Theory] + [InlineData("One", Numbers.One)] + [InlineData("FOUR", Numbers.Four)] + [InlineData("THREE", Numbers.Three)] + [InlineData("BOB", Numbers.Zero)] + public void ToEnum_without_override_can_convert_Numbers_enum_successfully(string text, Numbers expected) + { + var result = text.ToEnum(); + + result.Should().Be(expected); + ; + } + + [Theory] + [InlineData("One", Numbers.Five, Numbers.One)] + [InlineData("FOUR", Numbers.Five, Numbers.Four)] + [InlineData("THREE", Numbers.Five, Numbers.Three)] + [InlineData("BOB", Numbers.Five, Numbers.Five)] + public void ToEnum_with_override_can_convert_Numbers_enum_successfully(string text, Numbers defaultValue, Numbers expected) + { + var result = text.ToEnum(defaultValue); + + result.Should().Be(expected); + ; + } + } + + public class ToGuid + { + [Theory] + [InlineData("034B1998-E8C7-4DC0-B0EF-E4D606166756", "034B1998-E8C7-4DC0-B0EF-E4D606166756")] + [InlineData("034B1998-E8C7-4DC0-B0EF-E4D606166756", "034b1998-e8c7-4dc0-b0ef-e4d606166756")] + [InlineData("bob", "00000000-0000-0000-0000-000000000000")] + public void ToGuid_without_default_can_convert_successfully(string text, string expected) + { + var result = text.ToGuid(); + + result.ToString().Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData("034B1998-E8C7-4DC0-B0EF-E4D606166756", "00000000-0000-0000-0000-000000000000", "034B1998-E8C7-4DC0-B0EF-E4D606166756")] + [InlineData("034B1998-E8C7-4DC0-B0EF-E4D606166756", "00000000-0000-0000-0000-000000000000", "034b1998-e8c7-4dc0-b0ef-e4d606166756")] + [InlineData("034B1998-E8C7-4DC0-B0EF-E4D606166756", "E223D5C2-61C5-4FD9-AABE-F8761B4EDCA6", "034b1998-e8c7-4dc0-b0ef-e4d606166756")] + [InlineData("bob", "E223D5C2-61C5-4FD9-AABE-F8761B4EDCA6", "E223D5C2-61C5-4FD9-AABE-F8761B4EDCA6")] + public void ToGuid_with_default_can_convert_successfully(string text, string defaultValue, string expected) + { + var result = text.ToGuid(Guid.Parse(defaultValue)); + + result.ToString().Should().BeEquivalentTo(expected); + } + } + + public class To + { + [Fact] + public void To_without_default_for_null_class_instance_can_convert_object_successfully() + { + var instance = (TestClass2)null; + var result = instance.To(); + result.Should().BeNull(); + } + + [Fact] + public void To_without_default_for_related_class_can_convert_object_successfully() + { + var instance = new TestClass2(); + var result = instance.To(); + result.Should().NotBeNull(); + result.Should().Be(instance); + } + + [Fact] + public void To_without_default_for_unrelated_class_can_convert_object_successfully() + { + var instance = new TestClass3(); + var result = instance.To(); + result.Should().BeNull(); + } + + [Fact] + public void To_with_default_for_null_class_instance_can_convert_object_successfully() + { + var instance = (TestClass2)null; + var defaultValue = new TestClass1(); + var result = instance.To(defaultValue); + result.Should().NotBeNull(); + result.Should().Be(defaultValue); + } + + [Fact] + public void To_with_default_for_related_class_can_convert_object_successfully() + { + var defaultValue = new TestClass1(); + var instance = new TestClass2(); + var result = instance.To(defaultValue); + result.Should().NotBeNull(); + result.Should().Be(instance); + } + + [Fact] + public void To_with_default_for_unrelated_class_can_convert_object_successfully() + { + var defaultValue = new TestClass1(); + var instance = new TestClass3(); + var result = instance.To(defaultValue); + result.Should().NotBeNull(); + result.Should().Be(defaultValue); + } + } + } +} diff --git a/tests/DNX.Extensions.Tests/Conversion/GuidExtensionsTests.cs b/tests/DNX.Extensions.Tests/Conversion/GuidExtensionsTests.cs new file mode 100644 index 0000000..877a6db --- /dev/null +++ b/tests/DNX.Extensions.Tests/Conversion/GuidExtensionsTests.cs @@ -0,0 +1,48 @@ +using DNX.Extensions.Conversion; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace DNX.Extensions.Tests.Conversion +{ + public class GuidExtensionsTests(ITestOutputHelper outputHelper) + { + [Theory] + [InlineData("ABC")] + [InlineData("abc")] + [InlineData("")] + [InlineData(null)] + [InlineData("497111F7-1511-49A8-8DB2-31B5E40953CB")] + public void ToDeterministicGuid_can_create_a_guid_from_any_string(string text) + { + // Act + var result = text.ToDeterministicGuid(); + outputHelper.WriteLine($"Text: {text} = {result}"); + + // Assert + result.Should().NotBe(Guid.Empty); + result.ToString().Should().NotBe(text); + } + + [Theory] + [InlineData("ABC", "d2bd2f90-dfb1-4f0c-70b4-a5d23525e932")] + [InlineData("abc", "98500190-d23c-b04f-d696-3f7d28e17f72")] + [InlineData("", "d98c1dd4-008f-04b2-e980-0998ecf8427e")] + [InlineData(null, "d98c1dd4-008f-04b2-e980-0998ecf8427e")] + [InlineData("497111F7-1511-49A8-8DB2-31B5E40953CB", "811f4c34-f777-5718-e90f-5b03aee2b92f")] + [InlineData("Strings can be any length, and can be much longer than a standard Guid length", "151991b0-80dc-5902-d525-3a2f3c901477")] + public void ToDeterministicGuid_will_always_generate_a_predictable_result(string text, string expected) + { + // Act + var result = text.ToDeterministicGuid(); + var result2 = text.ToDeterministicGuid(); + outputHelper.WriteLine($"Text: {text} = {result}"); + + // Assert + result.Should().NotBe(Guid.Empty); + result.ToString().Should().NotBe(text); + result.Should().Be(result2); + result.ToString().Should().Be(expected); + } + } +} diff --git a/tests/DNX.Extensions.Tests/DNX.Extensions.Tests.csproj b/tests/DNX.Extensions.Tests/DNX.Extensions.Tests.csproj index ef324c8..f5b5536 100644 --- a/tests/DNX.Extensions.Tests/DNX.Extensions.Tests.csproj +++ b/tests/DNX.Extensions.Tests/DNX.Extensions.Tests.csproj @@ -8,6 +8,20 @@ disable + + + + + + + + + + + + + + all diff --git a/tests/DNX.Extensions.Tests/DateTimes/DateTimeExtensionsTests.cs b/tests/DNX.Extensions.Tests/DateTimes/DateTimeExtensionsTests.cs new file mode 100644 index 0000000..19710df --- /dev/null +++ b/tests/DNX.Extensions.Tests/DateTimes/DateTimeExtensionsTests.cs @@ -0,0 +1,156 @@ +using DNX.Extensions.DateTimes; +using FluentAssertions; +using Xunit; + +namespace DNX.Extensions.Tests.DateTimes +{ + public class DateTimeExtensionsTests + { + private static readonly Random Randomizer = new(); + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void SetYear_can_operate_as_expected(DateTime dateTime) + { + var year = Randomizer.Next(1, 9999); + var month = dateTime.Month; + var day = dateTime.Day; + + var result = dateTime.SetYear(year); + + result.Year.Should().Be(year); + result.Month.Should().Be(month); + result.Day.Should().Be(day); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void SetMonth_can_operate_as_expected(DateTime dateTime) + { + var year = dateTime.Year; + var month = Randomizer.Next(1, 12); + var day = dateTime.Day; + + var result = dateTime.SetMonth(month); + + result.Year.Should().Be(year); + result.Month.Should().Be(month); + result.Day.Should().Be(day); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void SetDay_can_operate_as_expected(DateTime dateTime) + { + var year = dateTime.Year; + var month = dateTime.Month; + var day = Randomizer.Next(1, 28); + + var result = dateTime.SetDay(day); + + result.Year.Should().Be(year); + result.Month.Should().Be(month); + result.Day.Should().Be(day); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void ResetHours_can_operate_as_expected(DateTime dateTime) + { + var result = dateTime.ResetHours(); + + result.Hour.Should().Be(0); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void ResetMinutes_can_operate_as_expected(DateTime dateTime) + { + var result = dateTime.ResetMinutes(); + + result.Minute.Should().Be(0); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void ResetSeconds_can_operate_as_expected(DateTime dateTime) + { + var result = dateTime.ResetSeconds(); + + result.Second.Should().Be(0); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void ResetMilliseconds_can_operate_as_expected(DateTime dateTime) + { + var result = dateTime.ResetMilliseconds(); + + result.Millisecond.Should().Be(0); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void SetHours_can_operate_as_expected(DateTime dateTime) + { + var value = Randomizer.Next(1, 24); + + var result = dateTime.SetHours(value); + + result.Hour.Should().Be(value); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void SetMinutes_can_operate_as_expected(DateTime dateTime) + { + var value = Randomizer.Next(1, 60); + + var result = dateTime.SetMinutes(value); + + result.Minute.Should().Be(value); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void SetSeconds_can_operate_as_expected(DateTime dateTime) + { + var value = Randomizer.Next(1, 60); + + var result = dateTime.SetSeconds(value); + + result.Second.Should().Be(value); + } + + [Theory] + [MemberData(nameof(ResetDateTime_Data))] + public void SetMilliseconds_can_operate_as_expected(DateTime dateTime) + { + var value = Randomizer.Next(1, 999); + + var result = dateTime.SetMilliseconds(value); + + result.Millisecond.Should().Be(value); + } + + public static TheoryData ResetDateTime_Data() + { + var data = new TheoryData + { + { DateTime.UtcNow }, + { DateTime.UnixEpoch }, + { DateTime.Parse("2021-11-05 20:53:44.12345") }, + { DateTime.Parse("2021-11-05 20:53:44") }, + { DateTime.Parse("2021-11-05 20:53") }, + { DateTime.Parse("2021-11-05") }, + }; + + if (DateTime.Now.Hour != DateTime.UtcNow.Hour) + { + data.Add(DateTime.Now); + } + + return data; + } + } +} diff --git a/tests/DNX.Extensions.Tests/Dictionaries/DictionaryExtensionsTests.cs b/tests/DNX.Extensions.Tests/Dictionaries/DictionaryExtensionsTests.cs new file mode 100644 index 0000000..9c02f5a --- /dev/null +++ b/tests/DNX.Extensions.Tests/Dictionaries/DictionaryExtensionsTests.cs @@ -0,0 +1,105 @@ +using DNX.Extensions.Dictionaries; +using FluentAssertions; +using Xunit; + +namespace DNX.Extensions.Tests.Dictionaries +{ + public class DictionaryExtensionsTests + { + [Theory] + [InlineData("Sunday", 0)] + [InlineData("Monday", 1)] + [InlineData("Thursday", 4)] + [InlineData("Saturday", 6)] + [InlineData("NotADay", 0)] + public void Can_get_dictionary_value_safely_or_default(string key, int expectedValue) + { + // Arrange + var dict = Enum.GetNames(typeof(DayOfWeek)) + .ToDictionary( + x => x, + x => (int)(Enum.Parse(x)) + ); + + // Act + var value = dict.Get(key); + + // Assert + value.Should().Be(expectedValue); + } + + [Theory] + [InlineData("Sunday", 999, 0)] + [InlineData("Monday", 999, 1)] + [InlineData("NotADay", 999, 999)] + [InlineData("NotADay", 0, 0)] + public void Can_get_dictionary_value_safely_overriding_default(string key, int defaultValue, int expectedValue) + { + // Arrange + var dict = Enum.GetNames(typeof(DayOfWeek)) + .ToDictionary( + x => x, + x => (int)(Enum.Parse(x)) + ); + + // Act + var value = dict.Get(key, defaultValue); + + // Assert + value.Should().Be(expectedValue); + } + + [Fact] + public void Can_split_well_formed_string_into_string_dictionary() + { + // Arrange + var text = "Liverpool=1|Southend United=7"; + + // Act + var result = text.ToStringDictionary(); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(typeof(Dictionary)); + result.Keys.Count.Should().Be(2); + result.ContainsKey("Liverpool").Should().Be(true); + result["Liverpool"].Should().Be("1"); + result.ContainsKey("Southend United").Should().Be(true); + result["Southend United"].Should().Be("7"); + } + + [Fact] + public void Null_string_is_handled_into_string_dictionary() + { + // Arrange + const string text = null; + + // Act + var result = text.ToStringDictionary(); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(typeof(Dictionary)); + result.Keys.Count.Should().Be(0); + } + + [Fact] + public void Can_split_well_formed_string_into_string_object_dictionary() + { + // Arrange + var text = "Liverpool=1|Southend United=7"; + + // Act + var result = text.ToStringObjectDictionary(); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(typeof(Dictionary)); + result.Keys.Count.Should().Be(2); + result.ContainsKey("Liverpool").Should().Be(true); + result["Liverpool"].Should().Be("1"); + result.ContainsKey("Southend United").Should().Be(true); + result["Southend United"].Should().Be("7"); + } + } +} diff --git a/tests/DNX.Extensions.Tests/Enumerations/EnumerableExtensionsTests.cs b/tests/DNX.Extensions.Tests/Enumerations/EnumerableExtensionsTests.cs deleted file mode 100644 index 876fd1e..0000000 --- a/tests/DNX.Extensions.Tests/Enumerations/EnumerableExtensionsTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using DNX.Extensions.Enumerations; -using FluentAssertions; -using Xunit; - -namespace DNX.Extensions.Tests.Enumerations; - -public class EnumerableExtensionsTests -{ - [Theory] - [InlineData("", false)] - [InlineData(null, false)] - [InlineData("a,b,c,d,e,f,g,h,i,j", true)] - public void Test_HasAny(string commaDelimitedArray, bool expectedResult) - { - var enumerable = commaDelimitedArray? - .Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); - - // Act - var result = enumerable.HasAny(); - - // Assert - result.Should().Be(expectedResult); - } - - [Theory] - [InlineData("", "1", false)] - [InlineData(null, "1", false)] - [InlineData("a1,b2,c1,d2,e1,f2,g1,h2,i1,j2", "1", true)] - [InlineData("a1,b2,c1,d2,e1,f2,g1,h2,i1,j2", "2", true)] - [InlineData("a1,b2,c1,d2,e1,f2,g1,h2,i1,j2", "0", false)] - public void Test_HasAny_predicate(string commaDelimitedArray, string suffix, bool expectedResult) - { - var enumerable = commaDelimitedArray? - .Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); - - // Act - var result = enumerable.HasAny(s => s.EndsWith(suffix)); - - // Assert - result.Should().Be(expectedResult); - } -} diff --git a/tests/DNX.Extensions.Tests/Enums/EnumExtensionsTests.cs b/tests/DNX.Extensions.Tests/Enums/EnumExtensionsTests.cs new file mode 100644 index 0000000..a64622a --- /dev/null +++ b/tests/DNX.Extensions.Tests/Enums/EnumExtensionsTests.cs @@ -0,0 +1,42 @@ +using System.ComponentModel; +using DNX.Extensions.Enums; +using FluentAssertions; +using Xunit; + +namespace DNX.Extensions.Tests.Enums +{ + public enum MyType + { + One = 1, + + [Description("Number 2")] + Two = 2, + + [Description] + Three = 3, + + [Description(null)] + Four = 4, + + [Description("")] + Five = 5 + } + + public class EnumExtensionsTests + { + [Theory] + [InlineData(MyType.One, "One")] + [InlineData(MyType.Two, "Number 2")] + [InlineData(MyType.Three, "")] + [InlineData(MyType.Four, null)] + [InlineData(MyType.Five, "")] + public void GetDescription_can_retrieve_value_correctly(MyType myType, string expectedResult) + { + // Act + var result = myType.GetDescription(); + + // Assert + result.Should().Be(expectedResult, $"{myType} has description: {result}"); + } + } +} diff --git a/tests/DNX.Extensions.Tests/Execution/RunSafelyTests.cs b/tests/DNX.Extensions.Tests/Execution/RunSafelyTests.cs new file mode 100644 index 0000000..bbfa8db --- /dev/null +++ b/tests/DNX.Extensions.Tests/Execution/RunSafelyTests.cs @@ -0,0 +1,318 @@ +using DNX.Extensions.Execution; +using FluentAssertions; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace DNX.Extensions.Tests.Execution +{ + public class RunSafelyTests + { + public class Execute_Tests + { + [Fact] + public void Can_run_simple_action_that_succeeds() + { + // Arrange + var guid = Guid.NewGuid(); + var value = Guid.Empty; + + // Act + RunSafely.Execute(() => value = guid); + + // Assert + value.Should().Be(guid); + } + + [Fact] + public void Can_handle_simple_action_that_fails() + { + // Act + RunSafely.Execute(() => throw new Exception(nameof(Can_handle_simple_action_that_fails))); + } + + [Fact] + public void Can_handle_simple_action_that_fails_and_extract_exception() + { + // Arrange + var value = int.MaxValue; + var guid = Guid.NewGuid(); + var message = ""; + + // Act + RunSafely.Execute(() => throw new Exception(guid.ToString()), ex => message = ex.Message); + + // Assert + value.Should().Be(int.MaxValue); + message.Should().NotBeNull(); + message.Should().NotBeNull(guid.ToString()); + } + + [Fact] + public void Can_handle_simple_func_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + + // Act + RunSafely.Execute(() => value = dividend / divisor); + + // Assert + value.Should().Be(0); + } + + [Fact] + public void Can_handle_simple_func_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + var message = ""; + + // Act + RunSafely.Execute(() => value = dividend / divisor, ex => message = ex.Message); + + // Assert + value.Should().Be(0); + message.Should().NotBeNullOrEmpty(); + message.Should().Contain("divide by zero"); + } + } + + public class ExecuteT_Tests + { + [Fact] + public void Can_run_simple_func_that_succeeds() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var value = RunSafely.Execute(() => guid); + + // Assert + value.Should().Be(guid); + } + + [Fact] + public void Can_handle_simple_func_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + + // Act + var value = RunSafely.Execute(() => dividend / divisor); + + // Assert + value.Should().Be(default); + } + + [Fact] + public void Can_handle_simple_func_with_default_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var defaultResult = 500; + + // Act + var value = RunSafely.Execute(() => dividend / divisor, defaultResult); + + // Assert + value.Should().Be(defaultResult); + } + + [Fact] + public void Can_handle_simple_func_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var message = ""; + + // Act + var value = RunSafely.Execute(() => dividend / divisor, ex => message = ex.Message); + + // Assert + value.Should().Be(default); + message.Should().NotBeNullOrEmpty(); + message.Should().Contain("divide by zero"); + } + + [Fact] + public void Can_handle_simple_func_with_default_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var defaultResult = 500; + var message = ""; + + // Act + var value = RunSafely.Execute(() => dividend / divisor, defaultResult, ex => message = ex.Message); + + // Assert + value.Should().Be(defaultResult); + message.Should().NotBeNullOrEmpty(); + message.Should().Contain("divide by zero"); + } + } + + public class ExecuteAsync_Tests + { + [Fact] + public async Task Can_run_simple_task_that_succeeds() + { + // Arrange + var dividend = 1000; + var divisor = 20; + var value = 0; + + var task = new Task(() => value = dividend / divisor); + task.Start(); + + // Act + await RunSafely.ExecuteAsync(task); + + // Assert + value.Should().Be(dividend / divisor); + } + + [Fact] + public async Task Can_handle_simple_action_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + + var task = new Task(() => value = dividend / divisor); + task.Start(); + + // Act + await RunSafely.ExecuteAsync(task); + + // Assert + value.Should().Be(0); + } + + [Fact] + public async Task Can_handle_simple_action_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + var message = ""; + + var task = new Task(() => value = dividend / divisor); + task.Start(); + + // Act + await RunSafely.ExecuteAsync(task, ex => message = ex.Message); + + // Assert + value.Should().Be(0); + message.Should().NotBeNullOrEmpty(); + message.Should().Contain("divide by zero"); + } + } + + public class ExecuteAsyncT_Tests + { + private static async Task DivideAsync(int dividend, int divisor) + { + var quotient = await Task.Run(() => dividend / divisor); + + return quotient; + } + + [Fact] + public async Task Can_run_simple_func_that_succeeds() + { + // Arrange + var dividend = 1000; + var divisor = 50; + + // Act + var value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor)); + + // Assert + value.Should().Be(dividend / divisor); + } + + [Fact] + public async Task Can_run_simple_func_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + + // Act + value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor)); + + // Assert + value.Should().Be(default); + } + + [Fact] + public async Task Can_run_simple_func_with_default_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var defaultResult = 500; + var value = 0; + + // Act + value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor), defaultResult); + + // Assert + value.Should().Be(defaultResult); + } + + [Fact] + public async Task Can_handle_simple_func_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + var message = ""; + + // Act + value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor), ex => message = ex.Message); + + // Assert + value.Should().Be(default); + message.Should().NotBeNull(); + message.Should().NotBeNull("divide by zero"); + } + + [Fact] + public async Task Can_handle_simple_func_with_default_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var defaultResult = 500; + var value = 0; + var message = ""; + + // Act + value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor), defaultResult, + ex => message = ex.Message); + + // Assert + value.Should().Be(defaultResult); + message.Should().NotBeNull(); + message.Should().Contain("divide by zero"); + } + } + } +} diff --git a/tests/DNX.Extensions.Tests/IO/DirectoryInfoExtensionsTests.cs b/tests/DNX.Extensions.Tests/IO/DirectoryInfoExtensionsTests.cs new file mode 100644 index 0000000..f511b91 --- /dev/null +++ b/tests/DNX.Extensions.Tests/IO/DirectoryInfoExtensionsTests.cs @@ -0,0 +1,269 @@ +using DNX.Extensions.IO; +using DNX.Extensions.Strings; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +// ReSharper disable StringLiteralTypo + +namespace DNX.Extensions.Tests.IO +{ + public class DirectoryInfoExtensionsTests : IDisposable + { + private readonly DirectoryInfo _directoryInfo; + private readonly ITestOutputHelper _outputHelper; + + public DirectoryInfoExtensionsTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + + var directoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + _directoryInfo = new DirectoryInfo(directoryPath); + _directoryInfo.Create(); + + SetupStandardFileStructure(_directoryInfo); + } + + public void Dispose() + { + _directoryInfo.Delete(true); + GC.SuppressFinalize(this); + } + + internal static void CreateFile(DirectoryInfo directoryInfo, string fileName) + { + var filePath = Path.Combine(directoryInfo.FullName, fileName); + + File.WriteAllText(filePath, string.Empty); + } + + internal static void SetupStandardFileStructure(DirectoryInfo directoryInfo) + { + var dir1 = directoryInfo.CreateSubdirectory("dir1"); + var dir2 = directoryInfo.CreateSubdirectory("dir2"); + var dir3 = dir1.CreateSubdirectory("dur3"); + var dir4 = dir2.CreateSubdirectory("dur4"); + + CreateFile(directoryInfo, "file.txt"); + CreateFile(directoryInfo, "file.json"); + CreateFile(dir1, "file1.txt"); + CreateFile(dir2, "file2.txt"); + CreateFile(dir1, "file1.json"); + CreateFile(dir2, "file2.json"); + CreateFile(dir3, "file1.tf"); + CreateFile(dir4, "file2.tf"); + } + + [Fact] + public void FindFiles_for_directory_that_does_not_exist_finds_expected_files() + { + // Arrange + const string pattern = "*.txt"; + var directoryInfo = new DirectoryInfo(Path.Join(Path.GetTempPath(), Guid.NewGuid().ToString())); + + // Act + var result = directoryInfo.FindFiles(pattern, false); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(0); + } + + [Fact] + public void FindFiles_for_single_pattern_without_recursion_finds_expected_files() + { + // Arrange + const string pattern = "*.txt"; + + // Act + var result = _directoryInfo.FindFiles(pattern, false); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(1); + result.Count(x => x.Name == "file.txt").Should().Be(1); + } + + [Fact] + public void FindFiles_for_single_pattern_with_recursion_finds_expected_files() + { + // Arrange + const string pattern = "*.txt"; + + // Act + var result = _directoryInfo.FindFiles(pattern, true); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(3); + result.Count(x => x.Name == "file.txt").Should().Be(1); + result.Count(x => x.Name == "file1.txt").Should().Be(1); + result.Count(x => x.Name == "file2.txt").Should().Be(1); + } + + [Fact] + public void FindFiles_for_multiple_patterns_without_recursion_finds_expected_files() + { + // Arrange + var patterns = new[] { "*.txt", "*.json" }; + SetupStandardFileStructure(_directoryInfo); + + // Act + var result = _directoryInfo.FindFiles(patterns, false); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(2); + result.Count(x => x.Name == "file.txt").Should().Be(1); + result.Count(x => x.Name == "file.json").Should().Be(1); + } + + [Fact] + public void FindFiles_for_multiple_patterns_with_recursion_finds_expected_files() + { + // Arrange + var patterns = new[] { "*.txt", "*.json" }; + SetupStandardFileStructure(_directoryInfo); + + // Act + var result = _directoryInfo.FindFiles(patterns, true); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(6); + result.Count(x => x.Name == "file.txt").Should().Be(1); + result.Count(x => x.Name == "file1.txt").Should().Be(1); + result.Count(x => x.Name == "file2.txt").Should().Be(1); + result.Count(x => x.Name == "file.json").Should().Be(1); + result.Count(x => x.Name == "file1.json").Should().Be(1); + result.Count(x => x.Name == "file2.json").Should().Be(1); + } + + [Fact] + public void FindDirectories_for_directory_that_does_not_exist_finds_expected_files() + { + // Arrange + const string pattern = "dir*"; + var directoryInfo = new DirectoryInfo(Path.Join(Path.GetTempPath(), Guid.NewGuid().ToString())); + + // Act + var result = directoryInfo.FindDirectories(pattern, false); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(0); + } + + [Fact] + public void FindDirectories_for_single_pattern_without_recursion_finds_expected_files() + { + // Arrange + const string pattern = "dir*"; + + // Act + var result = _directoryInfo.FindDirectories(pattern, false); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(2); + result.Count(x => x.Name == "dir1").Should().Be(1); + result.Count(x => x.Name == "dir2").Should().Be(1); + } + + [Fact] + public void FindDirectories_for_single_pattern_with_recursion_finds_expected_files() + { + // Arrange + const string pattern = "d?r*"; + + // Act + var result = _directoryInfo.FindDirectories(pattern, true); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(4); + result.Count(x => x.Name == "dir1").Should().Be(1); + result.Count(x => x.Name == "dir2").Should().Be(1); + result.Count(x => x.Name == "dur3").Should().Be(1); + result.Count(x => x.Name == "dur4").Should().Be(1); + } + + [Fact] + public void FindDirectories_for_multiple_patterns_without_recursion_finds_expected_files() + { + // Arrange + var patterns = new[] { "dir*", "dur*" }; + + // Act + var result = _directoryInfo.FindDirectories(patterns, false); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(2); + result.Count(x => x.Name == "dir1").Should().Be(1); + result.Count(x => x.Name == "dir2").Should().Be(1); + } + + [Fact] + public void FindDirectories_for_multiple_patterns_with_recursion_finds_expected_files() + { + // Arrange + var patterns = new[] { "dir*", "dur*" }; + + // Act + var result = _directoryInfo.FindDirectories(patterns, true); + + // Assert + result.Should().NotBeNull(); + result.Count().Should().Be(4); + result.Count(x => x.Name == "dir1").Should().Be(1); + result.Count(x => x.Name == "dir2").Should().Be(1); + result.Count(x => x.Name == "dur3").Should().Be(1); + result.Count(x => x.Name == "dur4").Should().Be(1); + } + + [Theory] + [MemberData(nameof(GetRelativePath_Data))] + public void GetRelativePath_can_extract_relative_path_correctly(string dirName, string relativeToDirName, string expected) + { + _outputHelper.WriteLine($"{Environment.OSVersion.Platform} - Checking DirName: {dirName} -> {relativeToDirName} = {expected}"); + + var dirInfo = dirName == null ? null : new DirectoryInfo(dirName); + var relativeToDirInfo = relativeToDirName == null ? null : new DirectoryInfo(relativeToDirName); + + var result = dirInfo.GetRelativePath(relativeToDirInfo); + + result.Should().Be(expected, $"{nameof(dirName)}: {dirName} - {nameof(relativeToDirInfo)}: {relativeToDirInfo}"); + } + + public static TheoryData GetRelativePath_Data() + { + var guid1 = Guid.NewGuid().ToString(); + var guid2 = Guid.NewGuid().ToString(); + var guid3 = Guid.NewGuid().ToString(); + + var data = new TheoryData() + { + { Path.Combine(Path.GetTempPath(), guid1), null, null }, + { null, Path.Combine(Path.GetTempPath(), guid1), null }, + { Path.Combine(Path.GetTempPath(), guid1), Path.Combine(Path.GetTempPath(), "abcdefg"), Path.Join("..", guid1) }, + { Path.Combine(Path.GetTempPath(), guid2), Path.Combine(Path.GetTempPath(), guid3), Path.Join("..", guid2) }, + { Path.Combine(Path.GetTempPath(), "abcdefg"), Path.Combine(Path.GetTempPath(), "abcdefg"), "" }, + { Path.Combine(Path.GetTempPath(), "abcdefg", "dir3"), Path.Combine(Path.GetTempPath(), "abcdefg"), "dir3" }, + { Path.Combine(Path.GetTempPath(), "abcdefg", "dir3"), Path.Combine(Path.GetTempPath(), "abcdefg", "dir3"), "" }, + { Path.Combine(Path.GetTempPath(), "folder1"), Path.Combine(Path.GetTempPath(), "folder2"), Path.Combine("..", "folder1") }, + }; + + if (Configuration.EnvironmentConfig.IsWindowsStyleFileSystem) + { + data.Add(Path.Combine(Path.GetTempPath(), "folder1"), Path.Combine("D:", "folder2"), Path.Combine(Path.GetTempPath(), "folder1")); + } + if (Configuration.EnvironmentConfig.IsLinuxStyleFileSystem) + { + data.Add(Path.Combine(Path.GetTempPath(), "folder1"), Path.Combine("/etc", "folder2"), Path.Combine(Path.GetTempPath(), "folder1").EnsureStartsWith(Path.Combine("..", ".."))); + } + + return data; + } + } +} diff --git a/tests/DNX.Extensions.Tests/IO/FileInfoExtensionsTests.cs b/tests/DNX.Extensions.Tests/IO/FileInfoExtensionsTests.cs new file mode 100644 index 0000000..c0a2aa0 --- /dev/null +++ b/tests/DNX.Extensions.Tests/IO/FileInfoExtensionsTests.cs @@ -0,0 +1,157 @@ +using DNX.Extensions.IO; +using DNX.Extensions.Strings; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace DNX.Extensions.Tests.IO +{ + public class FileInfoExtensionsTests(ITestOutputHelper outputHelper) + { + private static string DriveRoot1 + { + get + { + return Configuration.EnvironmentConfig.IsLinuxStyleFileSystem + ? "/root1" + : "C:"; + } + } + + private static string DriveRoot2 + { + get + { + return Configuration.EnvironmentConfig.IsLinuxStyleFileSystem + ? "/root2" + : "D:"; + } + } + + [Theory] + [MemberData(nameof(GetRelativeFileName_Data))] + public void GetRelativeFileName_can_extract_relative_filename_correctly(string fileName, string dirName, string expected) + { + outputHelper.WriteLine($"{Environment.OSVersion.Platform} - Checking FileName: {fileName} -> {dirName} = {expected}"); + + var fileInfo = new FileInfo(fileName); + var dirInfo = new DirectoryInfo(dirName); + + var result = fileInfo.GetRelativeFileName(dirInfo); + + result.Should().Be(expected, $"{nameof(dirName)}: {dirName} - {nameof(fileName)}: {fileName}"); + } + + [Theory] + [MemberData(nameof(GetRelativeFilePath_Data))] + public void GetRelativeFilePath_can_extract_relative_path_correctly(string fileName, string dirName, string expected) + { + outputHelper.WriteLine($"{Environment.OSVersion.Platform} - Checking FileName: {fileName} -> {dirName} = {expected}"); + + var fileInfo = new FileInfo(fileName); + var dirInfo = new DirectoryInfo(dirName); + + var result = fileInfo.GetRelativeFilePath(dirInfo); + + result.Should().Be(expected, $"{nameof(dirName)}: {dirName} - {nameof(fileName)}: {fileName}"); + } + + [Theory] + [MemberData(nameof(FileSizeData))] + public void GetFriendlyFileSize_given_a_fileSize_should_return_expected_text(long fileSize, string expected) + { + var result = FileInfoExtensions.GetFriendlyFileSize(fileSize); + + result.Should().Be(expected); + } + + [Fact] + public void GetFriendlyFileSize_given_an_invalid_FileInfo_should_return_expected_text() + { + // Arrange + var fileInfo = (FileInfo)null; + + // Act + var result = FileInfoExtensions.GetFriendlyFileSize(fileInfo); + + // Assert + result.Should().Be("0B"); + } + + [Theory] + [MemberData(nameof(FileSizeData))] + public void GetFriendlyFileSize_given_a_valid_FileInfo_should_return_expected_text(long fileSize, string expected) + { + var fileName = Path.GetTempFileName(); + var fileInfo = new FileInfo(fileName); + + var data = new byte[fileSize]; + + File.WriteAllBytes(fileInfo.FullName, data); + + // Act + var result = FileInfoExtensions.GetFriendlyFileSize(fileInfo); + + // Assert + result.Should().Be(expected); + + // Cleanup + fileInfo.Delete(); + } + + public static TheoryData FileSizeData() + { + return new TheoryData + { + { 0, "0B" }, + { 1000, "1000B" }, + { 1023, "1023B" }, + { 1024, "1KB" }, + { 1536, "1.5KB" }, + { 1792, "1.8KB" }, + { 2048, "2KB" }, + { 10240, "10KB" }, + { 102400, "100KB" }, + { 1024000, "1000KB" }, + { 1048500, "1023.9KB" }, + { 1048575, "1024KB" }, + { 1048576, "1MB" }, + { 2097152, "2MB" }, + { 10485760, "10MB" }, + }; + } + + public static TheoryData GetRelativeFileName_Data() + { + return new TheoryData + { + { Path.Combine(DriveRoot1, "Temp", "abcdefg", "file.txt"), Path.Combine(DriveRoot1, "Temp", "abcdefg"), "file.txt" }, + { Path.Combine(DriveRoot1, "Temp", "abcdefg", "dir3", "file1.tf"), Path.Combine(DriveRoot1, "Temp", "abcdefg"), Path.Combine("dir3", "file1.tf") }, + { Path.Combine(DriveRoot1, "Temp", "abcdefg", "dir3", "file1.tf"), Path.Combine(DriveRoot1, "Temp", "abcdefg", "dir3"), "file1.tf" }, + { Path.Combine(DriveRoot1, "Temp", "abcdefg", "dir3", "file1.tf"), Path.Combine(DriveRoot1, "Temp", "abcdefg", "dir3"), "file1.tf" }, + { Path.Combine(DriveRoot1, "Temp", "folder1", "file.txt"), Path.Combine(DriveRoot2, "folder2"), + Configuration.EnvironmentConfig.IsLinuxStyleFileSystem + ? Path.Combine(DriveRoot1, "Temp", "folder1", "file.txt").EnsureStartsWith(Path.Combine("..", "..")) + : Path.Combine(DriveRoot1, "Temp", "folder1", "file.txt") + }, + { Path.Combine(DriveRoot1, "Temp", "folder1", "file.txt"), Path.Combine(DriveRoot1, "Temp", "folder2"), Path.Combine("..", "folder1", "file.txt") }, + }; + } + + public static TheoryData GetRelativeFilePath_Data() + { + return new TheoryData + { + { Path.Combine(DriveRoot1, "Temp", "abcdefg", "file.txt"), Path.Combine(DriveRoot1, "Temp", "abcdefg"), "" }, + { Path.Combine(DriveRoot1, "Temp", "abcdefg", "dir3", "file1.tf"), Path.Combine(DriveRoot1, "Temp", "abcdefg"), "dir3" }, + { Path.Combine(DriveRoot1, "Temp", "abcdefg", "dir3", "file1.tf"), Path.Combine(DriveRoot1, "Temp", "abcdefg", "dir3"), "" }, + { Path.Combine(DriveRoot1, "Temp", "folder1", "file.txt"), Path.Combine(DriveRoot1, "Temp", "folder2"), Path.Combine("..", "folder1") }, + { Path.Combine(DriveRoot1, "Temp", "folder1", "file.txt"), Path.Combine(DriveRoot2, "folder2"), + Configuration.EnvironmentConfig.IsLinuxStyleFileSystem + ? Path.Combine(DriveRoot1, "Temp", "folder1").EnsureStartsWith(Path.Combine("..", "..")) + : Path.Combine(DriveRoot1, "Temp", "folder1") + }, + }; + } + } +} diff --git a/tests/DNX.Extensions.Tests/Linq/EnumerableExtensionsTests.cs b/tests/DNX.Extensions.Tests/Linq/EnumerableExtensionsTests.cs new file mode 100644 index 0000000..2a172df --- /dev/null +++ b/tests/DNX.Extensions.Tests/Linq/EnumerableExtensionsTests.cs @@ -0,0 +1,437 @@ +using System.Collections; +using System.Collections.Generic; +using DNX.Extensions.Linq; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +// ReSharper disable InconsistentNaming + +namespace DNX.Extensions.Tests.Linq +{ + internal enum OneToFive + { + One, + Two, + Three, + Four, + Five + } + + public class EnumerableExtensionsTests + { + public class HasAny + { + [Theory] + [InlineData("1,2,3,4,5")] + [InlineData("Dave,Bob,Steve")] + public void Populated_Enumerable_returns_successfully_as_True(string itemList) + { + // Arrange + var enumerable = itemList.Split(","); + + // Act + var result = enumerable.HasAny(); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void Empty_Enumerable_returns_successfully_as_False() + { + // Arrange + var enumerable = new List(); + + // Act + var result = enumerable.HasAny(); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void Null_Enumerable_returns_successfully_as_False() + { + // Arrange + List enumerable = null; + + // Act + var result = enumerable.HasAny(); + + // Assert + result.Should().BeFalse(); + } + } + + public class HasAny_Predicate + { + [Theory] + [InlineData("", "1", false)] + [InlineData(null, "1", false)] + [InlineData("a1,b2,c1,d2,e1,f2,g1,h2,i1,j2", "1", true)] + [InlineData("a1,b2,c1,d2,e1,f2,g1,h2,i1,j2", "2", true)] + [InlineData("a1,b2,c1,d2,e1,f2,g1,h2,i1,j2", "0", false)] + public void Test_HasAny_predicate(string commaDelimitedArray, string suffix, bool expectedResult) + { + var enumerable = commaDelimitedArray? + .Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries) + ; + + var result = enumerable.HasAny(s => s.EndsWith(suffix)); + + result.Should().Be(expectedResult); + } + } + + public class IsOneOf_Tests + { + [Theory] + [MemberData(nameof(IsOneOf_string_Data))] + public void IsOneOf_with_string_data_can_operate_as_expected(string[] list, string candidate, bool expectedResult) + { + var result = candidate.IsOneOf(list); + + result.Should().Be(expectedResult); + } + + [Theory] + [MemberData(nameof(IsOneOf_string_comparer_Data))] + public void IsOneOf_with_string_data_and_comparer_can_operate_as_expected(string[] list, string candidate, IEqualityComparer comparer, bool expectedResult) + { + var result = candidate.IsOneOf(list, comparer); + + result.Should().Be(expectedResult); + } + + [Fact] + public void IsOneOf_with_params_can_operate_as_expected() + { + ((string)null).IsOneOf().Should().Be(false); + ((string)null).IsOneOf((string[])null).Should().Be(false); + "Hello".IsOneOf((string[])null).Should().BeFalse(); + "Hello".IsOneOf().Should().Be(false); + "3".IsOneOf("1", "2", "3", "4", "5").Should().Be(true); + "6".IsOneOf("1", "2", "3", "4", "5").Should().Be(false); + "One".IsOneOf("One", "Two", "Three", "Four", "Five").Should().Be(true); + "Two".IsOneOf("One", "Two", "Three", "Four", "Five").Should().Be(true); + "Three".IsOneOf("One", "Two", "Three", "Four", "Five").Should().Be(true); + "Four".IsOneOf("One", "Two", "Three", "Four", "Five").Should().Be(true); + "Five".IsOneOf("One", "Two", "Three", "Four", "Five").Should().Be(true); + "five".IsOneOf("One", "Two", "Three", "Four", "Five").Should().Be(false); + "FIVE".IsOneOf("One", "Two", "Three", "Four", "Five").Should().Be(false); + "Six".IsOneOf("One", "Two", "Three", "Four", "Five").Should().Be(false); + 1.IsOneOf(1, 2, 3, 4, 5).Should().Be(true); + 3.IsOneOf(1, 2, 3, 4, 5).Should().Be(true); + 5.IsOneOf(1, 2, 3, 4, 5).Should().Be(true); + 6.IsOneOf(1, 2, 3, 4, 5).Should().Be(false); + OneToFive.One.IsOneOf(OneToFive.One, OneToFive.Three, OneToFive.Five).Should().Be(true); + OneToFive.Three.IsOneOf(OneToFive.One, OneToFive.Three, OneToFive.Five).Should().Be(true); + OneToFive.Five.IsOneOf(OneToFive.One, OneToFive.Three, OneToFive.Five).Should().Be(true); + OneToFive.Two.IsOneOf(OneToFive.One, OneToFive.Three, OneToFive.Five).Should().Be(false); + ((OneToFive)100).IsOneOf(OneToFive.One, OneToFive.Three, OneToFive.Five).Should().Be(false); + } + + [Fact] + public void IsOneOf_with_params_and_Comparer_can_operate_as_expected() + { + var comparer = StringComparer.FromComparison(StringComparison.CurrentCulture); + ((string)null).IsOneOf(comparer).Should().Be(false); + ((string)null).IsOneOf(comparer, null).Should().Be(false); + "Hello".IsOneOf((string[])null).Should().BeFalse(); + "Hello".IsOneOf().Should().Be(false); + "Hello".IsOneOf(comparer).Should().Be(false); + "3".IsOneOf(comparer, "1", "2", "3", "4", "5").Should().Be(true); + "6".IsOneOf(comparer, "1", "2", "3", "4", "5").Should().Be(false); + "One".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Two".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Three".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Four".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Five".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "five".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(false); + "FIVE".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(false); + "Six".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(false); + + comparer = StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase); + "Hello".IsOneOf(comparer).Should().Be(false); + "3".IsOneOf(comparer, "1", "2", "3", "4", "5").Should().Be(true); + "6".IsOneOf(comparer, "1", "2", "3", "4", "5").Should().Be(false); + "One".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Two".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Three".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Four".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Five".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "five".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "FIVE".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(true); + "Six".IsOneOf(comparer, "One", "Two", "Three", "Four", "Five").Should().Be(false); + } + + #region Test Data + + public static TheoryData IsOneOf_string_Data() + { + return new TheoryData + { + { null, "3", false }, + { Enumerable.Empty().ToArray(), "3", false }, + { "1,2,3,4,5".Split(','), "3", true }, + { "1,2,3,4,5".Split(','), "6", false }, + { "One,Two,Three,Four,Five".Split(','), "One", true }, + { "One,Two,Three,Four,Five".Split(','), "Two", true }, + { "One,Two,Three,Four,Five".Split(','), "Three", true }, + { "One,Two,Three,Four,Five".Split(','), "Four", true }, + { "One,Two,Three,Four,Five".Split(','), "Five", true }, + { "One,Two,Three,Four,Five".Split(','), "five", false }, + { "One,Two,Three,Four,Five".Split(','), "Six", false }, + }; + } + + public static TheoryData, bool> IsOneOf_string_comparer_Data() + { + return new TheoryData, bool> + { + { "1,2,3,4,5".Split(','), "3", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), true }, + { "1,2,3,4,5".Split(','), "6", StringComparer.FromComparison(StringComparison.CurrentCulture), false }, + { "1,2,3,4,5".Split(','), "3", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), true }, + { "1,2,3,4,5".Split(','), "6", StringComparer.FromComparison(StringComparison.CurrentCulture), false }, + + { "One,Two,Three,Four,Five".Split(','), "One", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Two", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Three", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Four", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Five", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "five", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Six", StringComparer.FromComparison(StringComparison.CurrentCultureIgnoreCase), false }, + { "One,Two,Three,Four,Five".Split(','), "One", StringComparer.FromComparison(StringComparison.CurrentCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "Two", StringComparer.FromComparison(StringComparison.CurrentCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "Three", StringComparer.FromComparison(StringComparison.CurrentCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "Four", StringComparer.FromComparison(StringComparison.CurrentCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "Five", StringComparer.FromComparison(StringComparison.CurrentCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "five", StringComparer.FromComparison(StringComparison.CurrentCulture), false }, + { "One,Two,Three,Four,Five".Split(','), "Six", StringComparer.FromComparison(StringComparison.CurrentCulture), false }, + + { "One,Two,Three,Four,Five".Split(','), "One", StringComparer.FromComparison(StringComparison.OrdinalIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Two", StringComparer.FromComparison(StringComparison.OrdinalIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Three", StringComparer.FromComparison(StringComparison.OrdinalIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Four", StringComparer.FromComparison(StringComparison.OrdinalIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Five", StringComparer.FromComparison(StringComparison.OrdinalIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "five", StringComparer.FromComparison(StringComparison.OrdinalIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Six", StringComparer.FromComparison(StringComparison.OrdinalIgnoreCase), false }, + { "One,Two,Three,Four,Five".Split(','), "One", StringComparer.FromComparison(StringComparison.Ordinal), true }, + { "One,Two,Three,Four,Five".Split(','), "Two", StringComparer.FromComparison(StringComparison.Ordinal), true }, + { "One,Two,Three,Four,Five".Split(','), "Three", StringComparer.FromComparison(StringComparison.Ordinal), true }, + { "One,Two,Three,Four,Five".Split(','), "Four", StringComparer.FromComparison(StringComparison.Ordinal), true }, + { "One,Two,Three,Four,Five".Split(','), "Five", StringComparer.FromComparison(StringComparison.Ordinal), true }, + { "One,Two,Three,Four,Five".Split(','), "five", StringComparer.FromComparison(StringComparison.Ordinal), false }, + { "One,Two,Three,Four,Five".Split(','), "Six", StringComparer.FromComparison(StringComparison.Ordinal), false }, + + { "One,Two,Three,Four,Five".Split(','), "One", StringComparer.FromComparison(StringComparison.InvariantCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Two", StringComparer.FromComparison(StringComparison.InvariantCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Three", StringComparer.FromComparison(StringComparison.InvariantCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Four", StringComparer.FromComparison(StringComparison.InvariantCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Five", StringComparer.FromComparison(StringComparison.InvariantCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "five", StringComparer.FromComparison(StringComparison.InvariantCultureIgnoreCase), true }, + { "One,Two,Three,Four,Five".Split(','), "Six", StringComparer.FromComparison(StringComparison.InvariantCultureIgnoreCase), false }, + { "One,Two,Three,Four,Five".Split(','), "One", StringComparer.FromComparison(StringComparison.InvariantCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "Two", StringComparer.FromComparison(StringComparison.InvariantCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "Three", StringComparer.FromComparison(StringComparison.InvariantCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "Four", StringComparer.FromComparison(StringComparison.InvariantCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "Five", StringComparer.FromComparison(StringComparison.InvariantCulture), true }, + { "One,Two,Three,Four,Five".Split(','), "five", StringComparer.FromComparison(StringComparison.InvariantCulture), false }, + { "One,Two,Three,Four,Five".Split(','), "Six", StringComparer.FromComparison(StringComparison.InvariantCulture), false }, + }; + } + + #endregion + } + + public class GetRandomItem(ITestOutputHelper testOutputHelper) + { + [Theory] + [InlineData("1,2,3,4,5", 10000)] + [InlineData("A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z", 10000)] + [InlineData("1,2", 100)] + [InlineData("1", 100)] + public void Populated_Enumerable_repeatedly_returns_an_item_successfully(string itemList, int count) + { + // Arrange + var enumerable = itemList.Split(",", StringSplitOptions.RemoveEmptyEntries); + + var hitCounts = enumerable + .ToDictionary(x => x, x => 0); + + Enumerable.Range(1, count) + .ToList() + .ForEach(x => + { + // Act + var result = enumerable.GetRandomItem(); + + hitCounts[result]++; + + // Assert + result.Should().NotBeNullOrEmpty(); + }); + + // Assert + foreach (var kvp in hitCounts) + { + testOutputHelper.WriteLine("HitCount [{0}]: {1}", kvp.Key, kvp.Value); + kvp.Value.Should().BeGreaterThan(0); + } + } + + [Fact] + public void Empty_string_Enumerable_returns_default() + { + // Arrange + var enumerable = Enumerable.Empty(); + + // Act + var result = enumerable.GetRandomItem(); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void Null_string_Enumerable_returns_default() + { + // Arrange + IEnumerable enumerable = null; + + // Act + var result = enumerable.GetRandomItem(); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void Empty_int_Enumerable_returns_default() + { + // Arrange + var enumerable = Enumerable.Empty(); + + // Act + var result = enumerable.GetRandomItem(); + + // Assert + result.Should().Be(default); + } + + [Fact] + public void Null_int_Enumerable_returns_default() + { + // Arrange + IEnumerable enumerable = null; + + // Act + var result = enumerable.GetRandomItem(); + + // Assert + result.Should().Be(default); + } + } + + public class GetAt + { + [Theory] + [InlineData("1,2,3,4,5", 0, "1")] + [InlineData("1,2,3,4,5", 2, "3")] + [InlineData("1,2,3,4,5", 4, "5")] + [InlineData("1,2,3,4,5", -1, null)] + [InlineData("1,2,3,4,5", 5, null)] + [InlineData("1,2,3,4,5", 6, null)] + [InlineData("1,2,3,4,5", 100, null)] + [InlineData("1,2,3,4,5", -100, null)] + public void Populated_List_repeatedly_returns_an_item_successfully(string itemList, int index, string expected) + { + // Arrange + var items = itemList.Split(",", StringSplitOptions.RemoveEmptyEntries) + .ToList(); + + // Act + var result = items.GetAt(index); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData("1,2,3,4,5", 0, "1")] + [InlineData("1,2,3,4,5", 2, "3")] + [InlineData("1,2,3,4,5", 4, "5")] + [InlineData("1,2,3,4,5", -1, null)] + [InlineData("1,2,3,4,5", 5, null)] + [InlineData("1,2,3,4,5", 6, null)] + [InlineData("1,2,3,4,5", 100, null)] + [InlineData("1,2,3,4,5", -100, null)] + public void Populated_Array_repeatedly_returns_an_item_successfully(string itemList, int index, string expected) + { + // Arrange + var items = itemList.Split(",", StringSplitOptions.RemoveEmptyEntries) + .ToArray(); + + // Act + var result = items.GetAt(index); + + // Assert + result.Should().Be(expected); + } + + [Fact] + public void Empty_IList_returns_default() + { + // Arrange + var items = Enumerable.Empty() + .ToList(); + + // Act + var result = items.GetAt(0); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void Empty_Array_returns_default() + { + // Arrange + var items = Array.Empty(); + + // Act + var result = items.GetAt(0); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void Null_IList_returns_default() + { + // Arrange + List items = null; + + // Act + var result = items.GetAt(0); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void Null_Array_returns_default() + { + // Arrange + string[] items = null; + + // Act + var result = items.GetAt(0); + + // Assert + result.Should().BeNull(); + } + } + } +} diff --git a/tests/DNX.Extensions.Tests/Reflection/ReflectionExtensionsTests.cs b/tests/DNX.Extensions.Tests/Reflection/ReflectionExtensionsTests.cs new file mode 100644 index 0000000..f728082 --- /dev/null +++ b/tests/DNX.Extensions.Tests/Reflection/ReflectionExtensionsTests.cs @@ -0,0 +1,136 @@ +using System.Reflection; +using DNX.Extensions.Reflection; +using FluentAssertions; +using Xunit; + +#pragma warning disable CA1822 // Members can be static +#pragma warning disable IDE0051 // Unused private member + +// ReSharper disable UnusedMember.Local + +namespace DNX.Extensions.Tests.Reflection; + +public class TestClass +{ + internal string MachineName => Environment.MachineName; + internal string UserName => Environment.UserName; + private string UserDomainName => Environment.UserDomainName; + + public string PublicSetOnly { private get; set; } + + private static string CurrentDirectory => Environment.CurrentDirectory; +} + +public class ReflectionExtensionsTests +{ + public static TheoryData GetPrivatePropertyValue_Private_Data() + { + return new TheoryData + { + { "UserDomainName", Environment.UserDomainName }, + }; + } + + public static TheoryData GetPrivatePropertyValue_Public_Data() + { + return new TheoryData + { + { nameof(TestClass.MachineName), Environment.MachineName }, + { nameof(TestClass.UserName), Environment.UserName }, + }; + } + + [Theory] + [MemberData(nameof(GetPrivatePropertyValue_Private_Data))] + public void GetPrivatePropertyValue_can_read_private_values_successfully(string propertyName, string expected) + { + // Arrange + var instance = new TestClass(); + + // Act + var result = instance.GetPrivatePropertyValue(propertyName); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [MemberData(nameof(GetPrivatePropertyValue_Public_Data))] + public void GetPrivatePropertyValue_can_read_non_private_values_successfully(string propertyName, string expected) + { + // Arrange + var instance = new TestClass(); + + // Act + var result = instance.GetPrivatePropertyValue(propertyName); + + // Assert + result.Should().Be(expected); + } + + [Fact] + public void GetPropertyValueByName_can_read_private_static_values_successfully() + { + // Arrange + var instance = new TestClass(); + + // Act + var result = instance.GetPropertyValueByName("CurrentDirectory", BindingFlags.Static | BindingFlags.NonPublic); + + // Assert + result.Should().Be(Environment.CurrentDirectory); + } + + [Fact] + public void GetPropertyValueByName_for_unknown_property_name_returns_null() + { + // Arrange + var instance = new TestClass(); + + // Act + var result = instance.GetPropertyValueByName(Guid.NewGuid().ToString(), BindingFlags.Static | BindingFlags.NonPublic); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void GetPropertyValueByName_for_property_name_without_getter_returns_null() + { + // Arrange + var instance = new TestClass(); + + // Act + var result = instance.GetPropertyValueByName(nameof(TestClass.PublicSetOnly), BindingFlags.Instance | BindingFlags.Public); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void GetPrivatePropertyValue_for_unknown_property_name_returns_null() + { + // Arrange + var instance = new TestClass(); + + // Act + var result = instance.GetPrivatePropertyValue(Guid.NewGuid().ToString()); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void GetPrivatePropertyValue_for_unknown_property_name_returns_default_value() + { + // Arrange + var instance = new TestClass(); + var defaultValue = Guid.NewGuid().ToString(); + + // Act + var result = instance.GetPrivatePropertyValue(Guid.NewGuid().ToString(), defaultValue); + + // Assert + result.Should().Be(defaultValue); + } +} diff --git a/tests/DNX.Extensions.Tests/Strings/ArgumentParserExtensionsTests.cs b/tests/DNX.Extensions.Tests/Strings/ArgumentParserExtensionsTests.cs new file mode 100644 index 0000000..80da711 --- /dev/null +++ b/tests/DNX.Extensions.Tests/Strings/ArgumentParserExtensionsTests.cs @@ -0,0 +1,36 @@ +using DNX.Extensions.Strings; +using FluentAssertions; +using Xunit; + +namespace DNX.Extensions.Tests.Strings; + +public class ArgumentParserExtensionsTests +{ + [Theory] + [InlineData("command value1 value2 --option optionValue", 5, "command|value1|value2|--option|optionValue")] + [InlineData("command value1 \"value2\" --option 'optionValue'", 5, "command|value1|value2|--option|'optionValue'")] + [InlineData("command \" value1 has multiple spaces \" \"value2 contains spaces\" --option 'optionValue'", 5, "command| value1 has multiple spaces |value2 contains spaces|--option|'optionValue'")] + [InlineData("--swiftcon \"Server=.\\SQLEXPRESS;Database=Swift;Trusted_Connection=True;ConnectRetryCount=6;ConnectRetryInterval=10;Connection Timeout=30;\" --swiftsoaphaaddress \"https://127.0.0.1:48200/soapha/\" --swiftencryptedlau mylauwhichshouldbeencrypted --institutionadclientid \"ab988c21-f419-4488-b3d6-a7ffeea63e68\" --institutionadclientsecret \"No8pQsZjBSIGbGMM6KCHf24qPvZ+YnvJKt0cTeQar0g=\" --institutionadtenantname \"cbiuktestinstitution.onmicrosoft.com\" --institutionprincipalcon \"Server=.\\SQLEXPRESS;Database=BankingInstitutionAuthentication;Trusted_Connection=True;ConnectRetryCount=6;ConnectRetryInterval=10;Connection Timeout=30;\" --institutionprincipalids \"AE261E74-4BDF-470C-9FFD-0227804DD8B9\" \"10246AE7-ED49-41C4-AF25-023521FF3622\"", 17, "--swiftcon|Server=.\\SQLEXPRESS;Database=Swift;Trusted_Connection=True;ConnectRetryCount=6;ConnectRetryInterval=10;Connection Timeout=30;|--swiftsoaphaaddress|https://127.0.0.1:48200/soapha/|--swiftencryptedlau|mylauwhichshouldbeencrypted|--institutionadclientid|ab988c21-f419-4488-b3d6-a7ffeea63e68|--institutionadclientsecret|No8pQsZjBSIGbGMM6KCHf24qPvZ+YnvJKt0cTeQar0g=|--institutionadtenantname|cbiuktestinstitution.onmicrosoft.com|--institutionprincipalcon|Server=.\\SQLEXPRESS;Database=BankingInstitutionAuthentication;Trusted_Connection=True;ConnectRetryCount=6;ConnectRetryInterval=10;Connection Timeout=30;|--institutionprincipalids|AE261E74-4BDF-470C-9FFD-0227804DD8B9|10246AE7-ED49-41C4-AF25-023521FF3622")] + [InlineData("Endpoint=sb://#{service_bus_url-prefix}#.servicebus.windows.net/;SharedAccessKeyName=#{servicebus_sas_applications_name}#;SharedAccessKey=#{servicebus_sas_applications_key}#", 1, "Endpoint=sb://#{service_bus_url-prefix}#.servicebus.windows.net/;SharedAccessKeyName=#{servicebus_sas_applications_name}#;SharedAccessKey=#{servicebus_sas_applications_key}#")] + [InlineData("\"Server=.;Database=JPMEmulator;Integrated Security=True;MultipleActiveResultSets=True\"", 1, "Server=.;Database=JPMEmulator;Integrated Security=True;MultipleActiveResultSets=True")] + public void When_called_with_a_valid_simple_string_of_values(string text, int parameterCount, string resultsByPipe) + { + // Act + var result = text.ParseArguments(); + + // Assert + result.Should().NotBeNull(); + result.Count.Should().Be(parameterCount); + + var parameters = resultsByPipe.Split("|".ToCharArray()); + parameters.Length.Should().Be(parameterCount); + + var parameterPosition = 0; + foreach (var parameter in parameters) + { + result[parameterPosition].Should().Be(parameter); + + ++parameterPosition; + } + } +} diff --git a/tests/DNX.Extensions.Tests/TestData/SampleData.json b/tests/DNX.Extensions.Tests/TestData/SampleData.json new file mode 100644 index 0000000..c592d02 --- /dev/null +++ b/tests/DNX.Extensions.Tests/TestData/SampleData.json @@ -0,0 +1,4 @@ +{ + "Id": 12345, + "Name": "Dave Dangerous" +}