diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx index ca80886..1719a33 100644 --- a/.docfx/Dockerfile.docfx +++ b/.docfx/Dockerfile.docfx @@ -1,4 +1,4 @@ -ARG NGINX_VERSION=1.30.0-alpine +ARG NGINX_VERSION=1.31.0-alpine FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base RUN rm -rf /usr/share/nginx/html/* diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d014a76..4cd6ee6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -180,6 +180,28 @@ Internal classes and methods must be validated by exercising the public API that - Public entry points provide sufficient coverage of internal code paths. - The internal implementation exists solely as a helper or utility for public-facing functionality. +## 10. ExcludeFromCodeCoverage Prohibition + +**Do not use `ExcludeFromCodeCoverage` attribute on any code.** This includes: + +- Test classes or test methods +- Production code +- Configuration code +- Any other code path + +### Rationale + +- Excluding code from coverage hides gaps and creates false confidence in test completeness. +- If a code path cannot or should not be tested, refactor the code to eliminate that path rather than hiding it from metrics. +- Every executable line should be covered by tests or be genuinely unreachable (dead code to be removed). + +### Alternative Approaches + +- **Untestable code paths**: Refactor to separate concerns and eliminate the untestable path. +- **External dependencies**: Use test doubles (fakes, stubs, spies) instead of excluding from coverage. +- **Configuration-only code**: Move to configuration files or extract into testable methods. +- **Generated or third-party code**: These should not be in the primary codebase; use NuGet packages or dedicated vendor folders if necessary. + --- description: 'Writing Performance Tests' applyTo: "tuning/**, **/*Benchmark*.cs" diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 1dc8b95..8aadd65 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -12,6 +12,10 @@ on: options: - Debug - Release + run_mac_tests: + type: boolean + description: Run the macOS test matrix despite the additional cost and runtime. + default: false permissions: contents: read @@ -21,6 +25,7 @@ jobs: name: initialize runs-on: ubuntu-24.04 outputs: + run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }} run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }} strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }} build-switches: ${{ steps.vars.outputs.build-switches }} @@ -29,6 +34,12 @@ jobs: name: calculate workflow variables shell: bash run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.run_mac_tests }}" == "true" ]]; then + echo "run-mac-tests=true" >> "$GITHUB_OUTPUT" + else + echo "run-mac-tests=false" >> "$GITHUB_OUTPUT" + fi + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT" echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT" @@ -101,10 +112,72 @@ jobs: build: true # required for xunitv3 download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + test_mac: + if: ${{ needs.init.outputs.run-mac-tests == 'true' }} + name: call-test-mac + needs: [init, build] + strategy: + fail-fast: false + matrix: + arch: [X64, ARM64] + configuration: [Debug, Release] + uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 + with: + runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }} + configuration: ${{ matrix.configuration }} + build-switches: -p:SkipSignAssembly=true + restore: true + build: true # required for xunitv3 + download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + + test_qualitygate: + if: ${{ always() }} + name: test-qualitygate + needs: [init, test_linux, test_windows, test_mac] + runs-on: ubuntu-24.04 + steps: + - name: Evaluate test results + shell: bash + env: + RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }} + TEST_LINUX_RESULT: ${{ needs.test_linux.result }} + TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }} + TEST_MAC_RESULT: ${{ needs.test_mac.result }} + run: | + require_success() { + local job_name="$1" + local job_result="$2" + + if [[ "$job_result" != "success" ]]; then + echo "::error::$job_name finished with '$job_result'." + exit 1 + fi + } + + require_success_or_skip() { + local job_name="$1" + local job_enabled="$2" + local job_result="$3" + + if [[ "$job_enabled" == "true" ]]; then + require_success "$job_name" "$job_result" + return + fi + + if [[ "$job_result" != "success" && "$job_result" != "skipped" ]]; then + echo "::error::$job_name finished with '$job_result' while disabled." + exit 1 + fi + } + + require_success "test_linux" "$TEST_LINUX_RESULT" + require_success "test_windows" "$TEST_WINDOWS_RESULT" + require_success_or_skip "test_mac" "$RUN_MAC_TESTS" "$TEST_MAC_RESULT" + sonarcloud: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} name: call-sonarcloud - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: organization: geekle @@ -113,18 +186,18 @@ jobs: secrets: inherit codecov: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} name: call-codecov - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 with: repository: codebeltnet/shared-kernel secrets: inherit codeql: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} name: call-codeql - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 permissions: security-events: write @@ -132,7 +205,7 @@ jobs: deploy: if: github.event_name != 'pull_request' name: call-nuget - needs: [build, pack, test_linux, test_windows, sonarcloud, codecov, codeql] + needs: [build, pack, test_qualitygate, sonarcloud, codecov, codeql] uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v3 with: version: ${{ needs.build.outputs.version }} diff --git a/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt b/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt index 63eb0fb..b9766b8 100644 --- a/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 0.5.7 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 0.5.6 Availability: .NET 10 and .NET 9 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8129f78..fdbb8de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. +## [0.5.7] - 2026-05-27 + +This is a patch release that improves test coverage, infrastructure resilience, and code quality through CI/CD enhancements, package updates, and developer guidance. + +### Added + +- Optional macOS test matrix in `ci-pipeline.yml` with conditional job gating, enabling ARM64 and X64 architecture testing when explicitly triggered. + +### Changed + +- `Microsoft.NET.Test.Sdk` upgraded to 18.6.0, +- `coverlet.collector` and `coverlet.msbuild` upgraded to 10.0.1, +- `.github/copilot-instructions.md` extended with comprehensive code coverage prohibition guidance and refactoring best practices, +- `ci-pipeline.yml` enhanced with test result evaluation quality gate and improved platform-specific test orchestration, +- Test coverage expanded across `TokenTest.cs`, `TimeToLiveTest.cs`, `CorrelationIdTest.cs`, `ClockSkewTest.cs`, and `SecretTest.cs` with additional test cases. + ## [0.5.6] - 2026-04-18 This is a service update that focuses on package dependencies. @@ -103,6 +119,7 @@ Purely an ALM release. No changes to the codebase. - CoordinatedUniversalTime record in the Codebelt.SharedKernel namespace that represents an object that can be used when you need a timestamp that is based on an absolute time (UTC) - TimeToLive record in the Codebelt.SharedKernel namespace that represents an object that can be used when issuing authentication tokens or similar (TTL) +[0.5.7]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.6...v0.5.7 [0.5.6]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.5...v0.5.6 [0.5.5]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.4...v0.5.5 [0.5.4]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.3...v0.5.4 diff --git a/Directory.Packages.props b/Directory.Packages.props index 098d261..9b39265 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,15 +3,15 @@ true - - - + + + - - - - - + + + + + diff --git a/test/Codebelt.SharedKernel.Tests/ClockSkewTest.cs b/test/Codebelt.SharedKernel.Tests/ClockSkewTest.cs index 77c5b3b..111e97d 100644 --- a/test/Codebelt.SharedKernel.Tests/ClockSkewTest.cs +++ b/test/Codebelt.SharedKernel.Tests/ClockSkewTest.cs @@ -77,6 +77,16 @@ public void FromSeconds_ShouldHaveThirtySecondsSkew() Assert.Equal(30, sut.Value.TotalSeconds); } + [Fact] + public void ConversionAndStringRepresentation_ShouldRepresentCorrectly() + { + ClockSkew sut = TimeSpan.FromSeconds(30); + TimeSpan actual = sut; + + Assert.Equal(TimeSpan.FromSeconds(30), actual); + Assert.Equal("00:00:30", sut.ToString()); + } + [Fact] public void Marshalling_ShouldRepresentCorrectly() { diff --git a/test/Codebelt.SharedKernel.Tests/CorrelationIdTest.cs b/test/Codebelt.SharedKernel.Tests/CorrelationIdTest.cs index aa32d17..4f8e5c1 100644 --- a/test/Codebelt.SharedKernel.Tests/CorrelationIdTest.cs +++ b/test/Codebelt.SharedKernel.Tests/CorrelationIdTest.cs @@ -100,6 +100,17 @@ public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenValueHasConsi Assert.Equal("0", sut.ActualValue); } + [Fact] + public void ConversionOperators_ShouldRepresentCorrectly() + { + var value = Guid.Parse("1acb4e49-28a6-4206-b22b-2392ffd4e605"); + CorrelationId fromGuid = value; + CorrelationId fromString = fromGuid.Value; + + Assert.Equal("1acb4e4928a64206b22b2392ffd4e605", fromGuid.Value); + Assert.Equal(fromGuid, fromString); + } + [Fact] public void Marshalling_ShouldRepresentCorrectly() { diff --git a/test/Codebelt.SharedKernel.Tests/Security/SecretTest.cs b/test/Codebelt.SharedKernel.Tests/Security/SecretTest.cs index 9a3e4e2..7909568 100644 --- a/test/Codebelt.SharedKernel.Tests/Security/SecretTest.cs +++ b/test/Codebelt.SharedKernel.Tests/Security/SecretTest.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text; using Cuemon; using Cuemon.Extensions.IO; using Codebelt.Extensions.Xunit; @@ -98,6 +99,15 @@ public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenValueHasExcee Assert.Equal("129 > 128", sut.ActualValue); } + [Fact] + public void StringAndByteRepresentation_ShouldRepresentCorrectly() + { + var sut = new Secret("1acb4e4928a64206b22b2392ffd4e605"); + + Assert.Equal("1acb4e4928a64206b22b2392ffd4e605", sut.ToString()); + Assert.Equal(Encoding.UTF8.GetBytes(sut.Value), sut.ToByteArray()); + } + [Fact] public void Marshalling_ShouldRepresentCorrectly() { diff --git a/test/Codebelt.SharedKernel.Tests/TimeToLiveTest.cs b/test/Codebelt.SharedKernel.Tests/TimeToLiveTest.cs index 27971bd..a747885 100644 --- a/test/Codebelt.SharedKernel.Tests/TimeToLiveTest.cs +++ b/test/Codebelt.SharedKernel.Tests/TimeToLiveTest.cs @@ -101,6 +101,16 @@ public void FromMinutes_ShouldHaveFifteenMinutesLifespan() Assert.Equal(15, sut.Value.TotalMinutes); } + [Fact] + public void ConversionAndStringRepresentation_ShouldRepresentCorrectly() + { + TimeToLive sut = TimeSpan.FromMinutes(15); + TimeSpan actual = sut; + + Assert.Equal(TimeSpan.FromMinutes(15), actual); + Assert.Equal("00:15:00", sut.ToString()); + } + [Fact] public void Marshalling_ShouldRepresentCorrectly() { diff --git a/test/Codebelt.SharedKernel.Tests/TokenTest.cs b/test/Codebelt.SharedKernel.Tests/TokenTest.cs index 3372637..dc18989 100644 --- a/test/Codebelt.SharedKernel.Tests/TokenTest.cs +++ b/test/Codebelt.SharedKernel.Tests/TokenTest.cs @@ -105,6 +105,16 @@ public void Constructor_ShouldNotThrowArgumentOutOfRangeException_WhenValueHasEx Assert.Equal(255, sut.Value.Length); } + [Fact] + public void Constructor_ShouldNotThrowArgumentOutOfRangeException_WhenMaximumCharacterFrequencyIsDisabled() + { + var sut = new Token(new string('a', 40), o => o.MaximumCharacterFrequency = 0); + + TestOutput.WriteLine(sut); + + Assert.Equal(40, sut.Value.Length); + } + [Fact] public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenValueHasHighCharacterFrequency() {