diff --git a/.github/workflows/delayed-pipelines.yml b/.github/workflows/delayed-pipelines.yml index 486193699..af88b86cb 100644 --- a/.github/workflows/delayed-pipelines.yml +++ b/.github/workflows/delayed-pipelines.yml @@ -19,48 +19,6 @@ on: - Release jobs: - prepare_linux: - name: 🐧 Prepare Linux - runs-on: ubuntu-22.04 - timeout-minutes: 15 - outputs: - restoreCacheKey: ${{ steps.dotnet-restore.outputs.restoreCacheKey }} - steps: - - name: Checkout - uses: codebeltnet/git-checkout@v1 - - - name: Install .NET - uses: codebeltnet/install-dotnet@v1 - with: - includePreview: true - - - id: dotnet-restore - name: Restore Dependencies - uses: codebeltnet/dotnet-restore@v2 - with: - useRestoreCache: true - - prepare_windows: - name: 🪟 Prepare Windows - runs-on: windows-2022 - timeout-minutes: 15 - outputs: - restoreCacheKey: ${{ steps.dotnet-restore.outputs.restoreCacheKey }} - steps: - - name: Checkout - uses: codebeltnet/git-checkout@v1 - - - name: Install .NET - uses: codebeltnet/install-dotnet@v1 - with: - includePreview: true - - - id: dotnet-restore - name: Restore Dependencies - uses: codebeltnet/dotnet-restore@v2 - with: - useRestoreCache: true - build: name: 🛠️ Build runs-on: ubuntu-22.04 @@ -69,7 +27,6 @@ jobs: matrix: configuration: [Debug, Release] framework: [net9.0, net8.0, netstandard2.0] - needs: [prepare_linux] outputs: version: ${{ steps.minver-calculate.outputs.version }} projects: ${{ steps.projects.outputs.result }} @@ -113,6 +70,9 @@ jobs: fi shell: bash + - name: Restore Dependencies + uses: codebeltnet/dotnet-restore@v2 + - id: dotnet-build name: Build for ${{ matrix.framework }} (${{ matrix.configuration }}) uses: codebeltnet/dotnet-build@v2 @@ -120,7 +80,6 @@ jobs: projects: ${{ steps.projects.outputs.result }} configuration: ${{ matrix.configuration }} framework: ${{ matrix.framework }} - restoreCacheKey: ${{ needs.prepare_linux.outputs.restoreCacheKey }} uploadBuildArtifactName: 'DelayedBuild.${{ matrix.framework }}.${{ matrix.configuration }}' pack: @@ -130,7 +89,7 @@ jobs: strategy: matrix: configuration: [Debug, Release] - needs: [prepare_linux, build] + needs: [build] steps: - name: Checkout uses: codebeltnet/git-checkout@v1 @@ -146,10 +105,11 @@ jobs: configuration: ${{ matrix.configuration }} uploadPackedArtifact: true version: ${{ needs.build.outputs.version }} - restoreCacheKey: ${{ needs.prepare_linux.outputs.restoreCacheKey }} downloadBuildArtifactPattern: 'DelayedBuild.*.${{ matrix.configuration }}' uploadPackedArtifactName: 'NuGet-Delayed-${{ matrix.configuration }}' - projects: ${{ needs.build.outputs.projects }} + projects: >- + src/**/Cuemon.Core.App.csproj + src/**/Cuemon.Extensions.Globalization.csproj deploy: if: github.event_name != 'pull_request' diff --git a/.github/workflows/pipelines.yml b/.github/workflows/pipelines.yml index 6c74ad378..e2adaef88 100644 --- a/.github/workflows/pipelines.yml +++ b/.github/workflows/pipelines.yml @@ -221,7 +221,48 @@ jobs: uploadPackedArtifact: true version: ${{ needs.build.outputs.version }} restoreCacheKey: ${{ needs.prepare_linux.outputs.restoreCacheKey }} - projects: ${{ needs.build.outputs.projects }} + projects: >- + src/**/Cuemon.AspNetCore.csproj + src/**/Cuemon.AspNetCore.App.csproj + src/**/Cuemon.AspNetCore.Authentication.csproj + src/**/Cuemon.AspNetCore.Mvc.csproj + src/**/Cuemon.AspNetCore.Razor.TagHelpers.csproj + src/**/Cuemon.Core.csproj + src/**/Cuemon.Data.csproj + src/**/Cuemon.Data.Integrity.csproj + src/**/Cuemon.Data.SqlClient.csproj + src/**/Cuemon.Diagnostics.csproj + src/**/Cuemon.Extensions.AspNetCore.csproj + src/**/Cuemon.Extensions.AspNetCore.Authentication.csproj + src/**/Cuemon.Extensions.AspNetCore.Mvc.csproj + src/**/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Text.Json.csproj + src/**/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Xml.csproj + src/**/Cuemon.Extensions.AspNetCore.Mvc.RazorPages.csproj + src/**/Cuemon.Extensions.AspNetCore.Text.Json.csproj + src/**/Cuemon.Extensions.AspNetCore.Xml.csproj + src/**/Cuemon.Extensions.Collections.Generic.csproj + src/**/Cuemon.Extensions.Collections.Specialized.csproj + src/**/Cuemon.Extensions.Core.csproj + src/**/Cuemon.Extensions.Data.csproj + src/**/Cuemon.Extensions.Data.Integrity.csproj + src/**/Cuemon.Extensions.DependencyInjection.csproj + src/**/Cuemon.Extensions.Diagnostics.csproj + src/**/Cuemon.Extensions.Hosting.csproj + src/**/Cuemon.Extensions.IO.csproj + src/**/Cuemon.Extensions.Net.csproj + src/**/Cuemon.Extensions.Reflection.csproj + src/**/Cuemon.Extensions.Runtime.Caching.csproj + src/**/Cuemon.Extensions.Text.csproj + src/**/Cuemon.Extensions.Text.Json.csproj + src/**/Cuemon.Extensions.Threading.csproj + src/**/Cuemon.Extensions.Xml.csproj + src/**/Cuemon.IO.csproj + src/**/Cuemon.Net.csproj + src/**/Cuemon.Resilience.csproj + src/**/Cuemon.Runtime.Caching.csproj + src/**/Cuemon.Security.Cryptography.csproj + src/**/Cuemon.Threading.csproj + src/**/Cuemon.Xml.csproj test: name: 🧪 Test @@ -262,7 +303,7 @@ jobs: projects: ${{ matrix.project }} configuration: ${{ matrix.configuration }} restoreCacheKey: ${{ runner.os == 'Linux' && needs.prepare_linux.outputs.restoreCacheKey || needs.prepare_windows.outputs.restoreCacheKey }} - buildSwitches: -p:SkipSignAssembly=true + buildSwitches: ${{ contains(matrix.project, 'Cuemon.Extensions.Globalization.Tests') && '-p:SkipSignAssembly=false' || '-p:SkipSignAssembly=true' }} testArguments: -- RunConfiguration.DisableAppDomain=true env: CONNECTIONSTRINGS__ADVENTUREWORKS: ${{ secrets.DB_ADVENTUREWORKS }} @@ -296,6 +337,13 @@ jobs: useRestoreCache: true restoreCacheKey: ${{ needs.prepare_linux.outputs.restoreCacheKey }} + - name: Download cuemon.snk file + uses: codebeltnet/gcp-download-file@v1 + with: + serviceAccountKey: ${{ secrets.GCP_TOKEN }} + bucketName: ${{ secrets.GCP_BUCKETNAME }} + objectName: cuemon.snk + - name: Run SonarCloud Analysis uses: codebeltnet/sonarcloud-scan@v1 with: @@ -308,7 +356,6 @@ jobs: uses: codebeltnet/dotnet-build@v2 with: uploadBuildArtifact: false - buildSwitches: -p:SkipSignAssembly=true - name: Finalize SonarCloud Analysis uses: codebeltnet/sonarcloud-scan-finalize@v1 @@ -353,11 +400,17 @@ jobs: - name: Prepare CodeQL SAST Analysis uses: codebeltnet/codeql-scan@v1 + - name: Download cuemon.snk file + uses: codebeltnet/gcp-download-file@v1 + with: + serviceAccountKey: ${{ secrets.GCP_TOKEN }} + bucketName: ${{ secrets.GCP_BUCKETNAME }} + objectName: cuemon.snk + - name: Build uses: codebeltnet/dotnet-build@v2 with: uploadBuildArtifact: false - buildSwitches: -p:SkipSignAssembly=true - name: Finalize CodeQL SAST Analysis uses: codebeltnet/codeql-scan-finalize@v1 diff --git a/Directory.Build.props b/Directory.Build.props index 9f4e016c2..2025b1d8b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ $([MSBuild]::IsOSPlatform('Windows')) true false - preview + latest diff --git a/Directory.Packages.props b/Directory.Packages.props index e391f3224..6318a7a12 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + diff --git a/src/Cuemon.Extensions.Globalization/Cuemon.Extensions.Globalization.csproj b/src/Cuemon.Extensions.Globalization/Cuemon.Extensions.Globalization.csproj index 40dd52c32..60d6a4383 100644 --- a/src/Cuemon.Extensions.Globalization/Cuemon.Extensions.Globalization.csproj +++ b/src/Cuemon.Extensions.Globalization/Cuemon.Extensions.Globalization.csproj @@ -1203,4 +1203,9 @@ + + + + + diff --git a/test/Cuemon.Extensions.Globalization.Tests/Cuemon.Extensions.Globalization.Tests.csproj b/test/Cuemon.Extensions.Globalization.Tests/Cuemon.Extensions.Globalization.Tests.csproj index 217ed8211..3ae2139fa 100644 --- a/test/Cuemon.Extensions.Globalization.Tests/Cuemon.Extensions.Globalization.Tests.csproj +++ b/test/Cuemon.Extensions.Globalization.Tests/Cuemon.Extensions.Globalization.Tests.csproj @@ -5,6 +5,7 @@ + diff --git a/test/Cuemon.Extensions.Globalization.Tests/YamlSerializerTest.cs b/test/Cuemon.Extensions.Globalization.Tests/YamlSerializerTest.cs new file mode 100644 index 000000000..934ec0356 --- /dev/null +++ b/test/Cuemon.Extensions.Globalization.Tests/YamlSerializerTest.cs @@ -0,0 +1,450 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.YamlDotNet; +using Codebelt.Extensions.YamlDotNet.Formatters; +using Cuemon.Extensions.IO; +using Cuemon.Extensions.Reflection; +using Xunit; +using Xunit.Abstractions; +using YamlDotNet.Core; +using YamlDotNet.Serialization.NamingConventions; + +namespace Cuemon.Extensions.Globalization +{ + public class YamlSerializerTest : Test + { + private readonly CultureInfo _cultureInfo; + + public YamlSerializerTest(ITestOutputHelper output) : base(output) + { + output.WriteLine("Assembly version: " + typeof(CultureInfoExtensions).Assembly.GetAssemblyVersion().ToString()); + try + { + _cultureInfo = new CultureInfo("da-DK").UseNationalLanguageSupport(); // from .NET6+ this is needed for both Windows and Linux; at least from pipeline (worked locally for Windows without Merge ...) + } + catch (Exception e) + { + output.WriteLine(e.ToString()); + } + } + + [Fact] + public void Serialize_ShouldSerializeDateFormatInfo() + { + var sut2 = _cultureInfo; + var sut3 = YamlFormatter.SerializeObject(sut2.DateTimeFormat, o => + { + o.Settings.NamingConvention = PascalCaseNamingConvention.Instance; + o.Settings.ScalarStyle = ScalarStyle.Plain; + o.Settings.IndentSequences = false; + o.Settings.FormatProvider = _cultureInfo; + o.Settings.Converters.Add(YamlConverterFactory.Create((writer, dt, _) => writer.WriteValue(dt.ToString(_cultureInfo)))); + }); + var sut4 = sut3.ToEncodedString(); + + TestOutput.WriteLine(sut4); + + var expected = @"AMDesignator: '' +Calendar: + MinSupportedDateTime: 01-01-0001 00:00:00 + MaxSupportedDateTime: 31-12-9999 23:59:59 + AlgorithmType: SolarCalendar + CalendarType: Localized + Eras: + - 1 + TwoDigitYearMax: {0} +DateSeparator: '-' +FirstDayOfWeek: Monday +CalendarWeekRule: FirstFourDayWeek +FullDateTimePattern: d. MMMM yyyy HH:mm:ss +LongDatePattern: d. MMMM yyyy +LongTimePattern: HH:mm:ss +MonthDayPattern: d. MMMM +PMDesignator: '' +RFC1123Pattern: ddd, dd MMM yyyy HH':'mm':'ss 'GMT' +ShortDatePattern: dd-MM-yyyy +ShortTimePattern: HH:mm +SortableDateTimePattern: yyyy'-'MM'-'dd'T'HH':'mm':'ss +TimeSeparator: ':' +UniversalSortableDateTimePattern: yyyy'-'MM'-'dd HH':'mm':'ss'Z' +YearMonthPattern: MMMM yyyy +AbbreviatedDayNames: +- sø +- ma +- ti +- on +- to +- fr +- lø +ShortestDayNames: +- sø +- ma +- ti +- on +- to +- fr +- lø +DayNames: +- søndag +- mandag +- tirsdag +- onsdag +- torsdag +- fredag +- lørdag +AbbreviatedMonthNames: +- jan +- feb +- mar +- apr +- maj +- jun +- jul +- aug +- sep +- okt +- nov +- dec +- '' +MonthNames: +- januar +- februar +- marts +- april +- maj +- juni +- juli +- august +- september +- oktober +- november +- december +- '' +NativeCalendarName: gregoriansk kalender +AbbreviatedMonthGenitiveNames: +- jan +- feb +- mar +- apr +- maj +- jun +- jul +- aug +- sep +- okt +- nov +- dec +- '' +MonthGenitiveNames: +- januar +- februar +- marts +- april +- maj +- juni +- juli +- august +- september +- oktober +- november +- december +- '' +".ReplaceLineEndings(); + +#if NET8_0_OR_GREATER + expected = string.Format(expected, "2049"); +#elif NET48_OR_GREATER + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + expected = expected.ReplaceAll("gregoriansk kalender", "dansk (Danmark)", StringComparison.Ordinal); + expected = string.Format(expected, "2029"); + } + else + { + expected = expected.ReplaceAll("gregoriansk", "Gregoriansk", StringComparison.Ordinal); + expected = string.Format(expected, "2049"); + } +#endif + + Assert.Equal(expected, sut4); + } + + [Fact] + public void Serialize_ShouldSerializeNumberFormatInfo() + { + var sut2 = _cultureInfo; + var sut3 = YamlFormatter.SerializeObject(sut2.NumberFormat, o => + { + o.Settings.ScalarStyle = ScalarStyle.DoubleQuoted; + o.Settings.IndentSequences = false; + o.Settings.FormatProvider = _cultureInfo; + o.Settings.NamingConvention = PascalCaseNamingConvention.Instance; + }); + var sut4 = sut3.ToEncodedString(); + + TestOutput.WriteLine(sut4); + + Assert.Equal(@"CurrencyDecimalDigits: 2 +CurrencyDecimalSeparator: "","" +CurrencyGroupSizes: +- 3 +NumberGroupSizes: +- 3 +PercentGroupSizes: +- 3 +CurrencyGroupSeparator: ""."" +CurrencySymbol: ""kr."" +NaNSymbol: ""NaN"" +CurrencyNegativePattern: 8 +NumberNegativePattern: 1 +PercentPositivePattern: 0 +PercentNegativePattern: 0 +NegativeInfinitySymbol: ""-∞"" +NegativeSign: ""-"" +NumberDecimalDigits: 2 +NumberDecimalSeparator: "","" +NumberGroupSeparator: ""."" +CurrencyPositivePattern: 3 +PositiveInfinitySymbol: ""∞"" +PositiveSign: ""+"" +PercentDecimalDigits: 2 +PercentDecimalSeparator: "","" +PercentGroupSeparator: ""."" +PercentSymbol: ""%"" +PerMilleSymbol: ""‰"" +NativeDigits: +- ""0"" +- ""1"" +- ""2"" +- ""3"" +- ""4"" +- ""5"" +- ""6"" +- ""7"" +- ""8"" +- ""9"" +DigitSubstitution: ""None"" +".ReplaceLineEndings(), sut4); + } + + + [Fact] + public void Serialize_ShouldSerializeCultureInfo() + { + var sut2 = _cultureInfo; + var sut3 = YamlFormatter.SerializeObject(sut2, o => + { + o.Settings.IndentSequences = false; + o.Settings.NamingConvention = PascalCaseNamingConvention.Instance; + o.Settings.Converters.Add(YamlConverterFactory.Create((writer, dt, _) => writer.WriteValue(dt.ToString(_cultureInfo)))); + }); + var sut4 = sut3.ToEncodedString().ReplaceLineEndings().Split(new[] { Environment.NewLine }, StringSplitOptions.None).ToList(); + + sut4.RemoveRange(sut4.FindIndex(s => s.StartsWith("CompareInfo")), 6); + sut4.RemoveRange(sut4.FindIndex(s => s.StartsWith("CultureTypes")), 1); + + var expected = @"LCID: 1030 +KeyboardLayoutId: 1030 +Name: da-DK +IetfLanguageTag: da-DK +DisplayName: Danish (Denmark) +NativeName: dansk (Danmark) +EnglishName: Danish (Denmark) +TwoLetterISOLanguageName: da +ThreeLetterISOLanguageName: dan +ThreeLetterWindowsLanguageName: DAN +TextInfo: + ANSICodePage: 1252 + OEMCodePage: 850 + MacCodePage: 10000 + EBCDICCodePage: 20277 + LCID: 1030 + CultureName: da-DK + ListSeparator: ; + IsRightToLeft: false +IsNeutralCulture: false +NumberFormat: + CurrencyDecimalDigits: 2 + CurrencyDecimalSeparator: ',' + CurrencyGroupSizes: + - 3 + NumberGroupSizes: + - 3 + PercentGroupSizes: + - 3 + CurrencyGroupSeparator: . + CurrencySymbol: kr. + NaNSymbol: NaN + CurrencyNegativePattern: 8 + NumberNegativePattern: 1 + PercentPositivePattern: 0 + PercentNegativePattern: 0 + NegativeInfinitySymbol: -∞ + NegativeSign: '-' + NumberDecimalDigits: 2 + NumberDecimalSeparator: ',' + NumberGroupSeparator: . + CurrencyPositivePattern: 3 + PositiveInfinitySymbol: ∞ + PositiveSign: + + PercentDecimalDigits: 2 + PercentDecimalSeparator: ',' + PercentGroupSeparator: . + PercentSymbol: '%' + PerMilleSymbol: ‰ + NativeDigits: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + DigitSubstitution: None +DateTimeFormat: + AMDesignator: '' + Calendar: + MinSupportedDateTime: 01-01-0001 00:00:00 + MaxSupportedDateTime: 31-12-9999 23:59:59 + AlgorithmType: SolarCalendar + CalendarType: Localized + Eras: + - 1 + TwoDigitYearMax: {0} + DateSeparator: '-' + FirstDayOfWeek: Monday + CalendarWeekRule: FirstFourDayWeek + FullDateTimePattern: d. MMMM yyyy HH:mm:ss + LongDatePattern: d. MMMM yyyy + LongTimePattern: HH:mm:ss + MonthDayPattern: d. MMMM + PMDesignator: '' + RFC1123Pattern: ddd, dd MMM yyyy HH':'mm':'ss 'GMT' + ShortDatePattern: dd-MM-yyyy + ShortTimePattern: HH:mm + SortableDateTimePattern: yyyy'-'MM'-'dd'T'HH':'mm':'ss + TimeSeparator: ':' + UniversalSortableDateTimePattern: yyyy'-'MM'-'dd HH':'mm':'ss'Z' + YearMonthPattern: MMMM yyyy + AbbreviatedDayNames: + - sø + - ma + - ti + - on + - to + - fr + - lø + ShortestDayNames: + - sø + - ma + - ti + - on + - to + - fr + - lø + DayNames: + - søndag + - mandag + - tirsdag + - onsdag + - torsdag + - fredag + - lørdag + AbbreviatedMonthNames: + - jan + - feb + - mar + - apr + - maj + - jun + - jul + - aug + - sep + - okt + - nov + - dec + - '' + MonthNames: + - januar + - februar + - marts + - april + - maj + - juni + - juli + - august + - september + - oktober + - november + - december + - '' + NativeCalendarName: gregoriansk kalender + AbbreviatedMonthGenitiveNames: + - jan + - feb + - mar + - apr + - maj + - jun + - jul + - aug + - sep + - okt + - nov + - dec + - '' + MonthGenitiveNames: + - januar + - februar + - marts + - april + - maj + - juni + - juli + - august + - september + - oktober + - november + - december + - '' +Calendar: + MinSupportedDateTime: 01-01-0001 00:00:00 + MaxSupportedDateTime: 31-12-9999 23:59:59 + AlgorithmType: SolarCalendar + CalendarType: Localized + Eras: + - 1 + TwoDigitYearMax: {0} +OptionalCalendars: +- MinSupportedDateTime: 01-01-0001 00:00:00 + MaxSupportedDateTime: 31-12-9999 23:59:59 + AlgorithmType: SolarCalendar + CalendarType: Localized + Eras: + - 1 + TwoDigitYearMax: {0} +UseUserOverride: true +"; + +#if NET8_0_OR_GREATER || NET48_OR_GREATER + expected = string.Format(expected, "2049"); +#else + expected = string.Format(expected, "2029"); +#endif + +#if NET48_OR_GREATER + expected = expected.ReplaceAll("gregoriansk", "Gregoriansk", StringComparison.Ordinal); +#endif + + TestOutput.WriteLines(sut4); + + Assert.Equal(expected.ReplaceLineEndings().Split(new[] { Environment.NewLine }, StringSplitOptions.None).ToList(), sut4); + } + } +}