From f892a283fe66331ef12ef41fea27dafa6972f6a9 Mon Sep 17 00:00:00 2001 From: "pi.admin" Date: Sun, 24 May 2026 18:08:55 -0700 Subject: [PATCH 1/6] Update --- .github/actions/push-package/action.yml | 2 +- .github/workflows/build-test-cross.yml | 10 ++++++++++ .github/workflows/build-test-push.yml | 8 ++++---- README.md | 4 ++-- build/README.md | 14 +++++++------- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/actions/push-package/action.yml b/.github/actions/push-package/action.yml index cb0ab02..3c774ec 100644 --- a/.github/actions/push-package/action.yml +++ b/.github/actions/push-package/action.yml @@ -9,7 +9,7 @@ inputs: default: 'false' required: false config: - description: 'The build configuration (Build or Release)' + description: 'The build configuration (Debug or Release)' default: Release required: true version: diff --git a/.github/workflows/build-test-cross.yml b/.github/workflows/build-test-cross.yml index 5953972..14926fa 100644 --- a/.github/workflows/build-test-cross.yml +++ b/.github/workflows/build-test-cross.yml @@ -66,6 +66,11 @@ jobs: - name: Checkout uses: actions/checkout@v6 + # Read env + - name: Read Env + run: cat .github/package_version.env >> $GITHUB_ENV + continue-on-error: false + # Setup .Net with global.json - name: Setup .NET uses: actions/setup-dotnet@v5 @@ -99,6 +104,11 @@ jobs: - name: Checkout uses: actions/checkout@v6 + # Read env + - name: Read Env + run: cat .github/package_version.env >> $GITHUB_ENV + continue-on-error: false + # Setup .Net with global.json - name: Setup .NET uses: actions/setup-dotnet@v5 diff --git a/.github/workflows/build-test-push.yml b/.github/workflows/build-test-push.yml index f3cc366..dd18b38 100644 --- a/.github/workflows/build-test-push.yml +++ b/.github/workflows/build-test-push.yml @@ -161,8 +161,8 @@ jobs: gistID: ${{ secrets.PI_GITHUB_GIST_ID }} filename: coverage_${{ github.event.repository.name }}_line.json label: coverage-line - message: ${{ steps.oneimlx-shared.outputs.coverage-line }}% - valColorRange: ${{ steps.oneimlx-shared.outputs.coverage-line }} + message: ${{ steps.shared.outputs.coverage-line }}% + valColorRange: ${{ steps.shared.outputs.coverage-line }} maxColorRange: 100 minColorRange: 0 @@ -174,7 +174,7 @@ jobs: gistID: ${{ secrets.PI_GITHUB_GIST_ID }} filename: coverage_${{ github.event.repository.name }}_branch.json label: coverage-branch - message: ${{ steps.oneimlx-shared.outputs.coverage-branch }}% - valColorRange: ${{ steps.oneimlx-shared.outputs.coverage-branch }} + message: ${{ steps.shared.outputs.coverage-branch }}% + valColorRange: ${{ steps.shared.outputs.coverage-branch }} maxColorRange: 100 minColorRange: 0 diff --git a/README.md b/README.md index 9c22ab4..804cbb1 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ This repository contains the shared components for our cross-platform frameworks [![NuGet](https://img.shields.io/nuget/vpre/OneImlx.Test?label=OneImlx.Test)](https://www.nuget.org/packages/OneImlx.Test) ## Status -[![Cross-Platform Build and Test](https://github.com/perpetualintelligence/shared/actions/workflows/build-test-cross.yml/badge.svg)](https://github.com/perpetualintelligence/shared/actions/workflows/build-test-cross-manual.yml) -[![Push Build and Test](https://github.com/perpetualintelligence/shared/actions/workflows/build-test-push.yml/badge.svg)](https://github.com/perpetualintelligence/shared/actions/workflows/build-test-push.yml) +[![build-test-cross](https://github.com/perpetualintelligence/shared/actions/workflows/build-test-cross.yml/badge.svg)](https://github.com/perpetualintelligence/shared/actions/workflows/build-test-cross.yml) +[![build-test-push](https://github.com/perpetualintelligence/shared/actions/workflows/build-test-push.yml/badge.svg)](https://github.com/perpetualintelligence/shared/actions/workflows/build-test-push.yml) ![coverage-line](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/perpetualintelligencegit/141903832ee52f1e9e7913300a92d507/raw/coverage_shared_line.json) ![coverage-branch](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/perpetualintelligencegit/141903832ee52f1e9e7913300a92d507/raw/coverage_shared_branch.json) diff --git a/build/README.md b/build/README.md index f6d551a..0cf4ea1 100644 --- a/build/README.md +++ b/build/README.md @@ -1,26 +1,26 @@ # Build ## Local Machine -Follow the steps to set up the `pi-cli` repository on your local development machine. +Follow the steps to set up the `shared` repository on your local development machine. -1. Download and install [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) -2. Clone the [protocols](https://github.com/perpetualintelligence/protocols) GitHub repo +1. Download and install [Visual Studio 2026](https://visualstudio.microsoft.com/vs/) +2. Clone the [shared](https://github.com/perpetualintelligence/shared) GitHub repo 3. Set PI_CI_REFERENCE environment variable to `cross` ## CICD This workflow folder contains the build and deployment pipelines for generating and publishing [Nuget](https://www.nuget.org/profiles/perpetualintelligencellc) and [GitHub](https://github.com/orgs/perpetualintelligence/packages?repo_name=data) packages. -- *build-test-cross-manual*: The manual action that builds and tests the code changes on Windows, Linux and macOS. -- *build-test-publish*: The automated action that publishes the packages to [Nuget](https://www.nuget.org/profiles/perpetualintelligencellc) and [GitHub](https://github.com/orgs/perpetualintelligence/packages?repo_name=data), see [releases](https://github.com/perpetualintelligence/cli/releases) +- *build-test-cross*: The manual action that builds and tests the code changes on Windows, Linux and macOS. +- *build-test-push*: The automated action that publishes the packages to [Nuget](https://www.nuget.org/profiles/perpetualintelligencellc) and [GitHub](https://github.com/orgs/perpetualintelligence/packages?repo_name=data), see [releases](https://github.com/perpetualintelligence/shared/releases) - *delete-packages*: The automated action cleans the packages every week and keeps the latest working version. For stable versions, refer to [Nuget](https://www.nuget.org/profiles/perpetualintelligencellc) packages. -> ***Note: The `build-test-publish` release to Nuget pipeline triggers a deployment approval.*** +> ***Note: The `build-test-push` release to Nuget pipeline triggers a deployment approval.*** ## Versioning All packages follow [sematic](https://semver.org/) versioning schemes. The env file *package_version.env* defines the package versions. ## Project Dependencies -The *PI_CI_REFERENCE* environment variable defines how *.csproj* references the dependencies for CI and local development. It supportes the following values: +The *PI_CI_REFERENCE* environment variable defines how *.csproj* references the dependencies for CI and local development. It supports the following values: - *local*: Project references for local development within the same repo - *cross*: Project references for local development across repos - *package*: Package references for CI/CD and deployment From 559e5698a6f348803a9c43e592aa61f85a8e7ad4 Mon Sep 17 00:00:00 2001 From: "pi.admin" Date: Sun, 24 May 2026 18:36:19 -0700 Subject: [PATCH 2/6] Submit --- .github/actions/push-package/action.yml | 26 +----------------- .github/workflows/build-test-push.yml | 36 +++++++++++++++++-------- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/.github/actions/push-package/action.yml b/.github/actions/push-package/action.yml index 3c774ec..580fe0c 100644 --- a/.github/actions/push-package/action.yml +++ b/.github/actions/push-package/action.yml @@ -25,13 +25,6 @@ inputs: description: 'The package registry (github or nuget)' default: github required: true -outputs: - coverage-line: - description: 'Line coverage percentage' - value: ${{ steps.extract-coverage.outputs.line }} - coverage-branch: - description: 'Branch coverage percentage' - value: ${{ steps.extract-coverage.outputs.branch }} runs: using: "composite" steps: @@ -41,29 +34,12 @@ runs: run: dotnet build ./src/${{ inputs.project }}/${{ inputs.project }}.csproj -c ${{ inputs.config }} --verbosity minimal shell: bash - # Test + # Test and collect coverage - name: Test if: ${{ inputs.skiptest == 'false' }} run: dotnet test ./test/${{ inputs.project }}.Tests/${{ inputs.project }}.Tests.csproj -c ${{ inputs.config }} --verbosity minimal --collect:"XPlat Code Coverage" --results-directory ./coverage shell: bash - # Generate coverage report - - name: Coverage Report - if: ${{ inputs.skiptest == 'false' }} - run: reportgenerator -reports:./coverage/**/coverage.cobertura.xml -targetdir:./coverage/report -reporttypes:JsonSummary - shell: bash - - # Extract line and branch coverage percentages - - name: Extract Coverage - id: extract-coverage - if: ${{ inputs.skiptest == 'false' }} - run: | - LINE=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['linecoverage']))") - BRANCH=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['branchcoverage']))") - echo "line=$LINE" >> $GITHUB_OUTPUT - echo "branch=$BRANCH" >> $GITHUB_OUTPUT - shell: bash - # Pack - name: Pack run: | diff --git a/.github/workflows/build-test-push.yml b/.github/workflows/build-test-push.yml index dd18b38..19a35ee 100644 --- a/.github/workflows/build-test-push.yml +++ b/.github/workflows/build-test-push.yml @@ -6,7 +6,7 @@ name: build-test-push # 1 release at a time -concurrency: shared +concurrency: ${{ github.event.repository.name }} # CICD. The rc and release are a manual trigger on: @@ -116,14 +116,13 @@ jobs: run: dotnet tool install --global dotnet-reportgenerator-globaltool continue-on-error: false - # Setup local - # OneImlx.Shared is the first package so we use the local project reference to avoid cyclic Nuget dependency resolution. + # OneImlx.Shared is the first package so we use the cross project reference to avoid cyclic Nuget dependency resolution. # See OneImlx.Shared.Test.csproj for more info. - - name: Force local + - name: Force cross run: echo "PI_CI_REFERENCE=cross" >> $GITHUB_ENV continue-on-error: false - # Publish OneImlx.Shared + # Publish OneImlx.Shared (deposits coverage into ./coverage) - name: OneImlx.Shared id: oneimlx-shared uses: ./.github/actions/push-package @@ -135,12 +134,12 @@ jobs: nugetapikey: ${{ env.PI_NUGET_PAT_ENV }} registry: ${{ env.PI_PUBLISH_REGISTRY }} - # Setup package + # Switch to package references for downstream packages - name: Force package run: echo "PI_CI_REFERENCE=package" >> $GITHUB_ENV continue-on-error: false - # Publish OneImlx.Test + # Publish OneImlx.Test (no tests, no coverage) - name: OneImlx.Test id: oneimlx-test uses: ./.github/actions/push-package @@ -153,6 +152,21 @@ jobs: nugetapikey: ${{ env.PI_NUGET_PAT_ENV }} registry: ${{ env.PI_PUBLISH_REGISTRY }} + # Aggregate coverage report across all packages deposited into ./coverage + - name: Coverage Report + run: reportgenerator -reports:./coverage/**/coverage.cobertura.xml -targetdir:./coverage/report -reporttypes:JsonSummary + shell: bash + + # Extract aggregate line and branch coverage + - name: Extract Coverage + id: coverage + run: | + LINE=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['linecoverage']))") + BRANCH=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['branchcoverage']))") + echo "line=$LINE" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + shell: bash + # Update line coverage badge in Gist - name: Line Coverage Badge uses: Schneegans/dynamic-badges-action@v1.8.0 @@ -161,8 +175,8 @@ jobs: gistID: ${{ secrets.PI_GITHUB_GIST_ID }} filename: coverage_${{ github.event.repository.name }}_line.json label: coverage-line - message: ${{ steps.shared.outputs.coverage-line }}% - valColorRange: ${{ steps.shared.outputs.coverage-line }} + message: ${{ steps.coverage.outputs.line }}% + valColorRange: ${{ steps.coverage.outputs.line }} maxColorRange: 100 minColorRange: 0 @@ -174,7 +188,7 @@ jobs: gistID: ${{ secrets.PI_GITHUB_GIST_ID }} filename: coverage_${{ github.event.repository.name }}_branch.json label: coverage-branch - message: ${{ steps.shared.outputs.coverage-branch }}% - valColorRange: ${{ steps.shared.outputs.coverage-branch }} + message: ${{ steps.coverage.outputs.branch }}% + valColorRange: ${{ steps.coverage.outputs.branch }} maxColorRange: 100 minColorRange: 0 From ef01048e24753938159d97636026e62bc7441fec Mon Sep 17 00:00:00 2001 From: "pi.admin" Date: Sun, 24 May 2026 18:42:55 -0700 Subject: [PATCH 3/6] fix coverage numbers --- .github/workflows/build-test-push.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test-push.yml b/.github/workflows/build-test-push.yml index 19a35ee..83fca75 100644 --- a/.github/workflows/build-test-push.yml +++ b/.github/workflows/build-test-push.yml @@ -161,8 +161,8 @@ jobs: - name: Extract Coverage id: coverage run: | - LINE=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['linecoverage']))") - BRANCH=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['branchcoverage']))") + LINE=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['lineCoverage']))") + BRANCH=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['branchCoverage']))") echo "line=$LINE" >> $GITHUB_OUTPUT echo "branch=$BRANCH" >> $GITHUB_OUTPUT shell: bash From 8d20f81a467b5ffeaf037df9d0b9ad2fb015e8dc Mon Sep 17 00:00:00 2001 From: "pi.admin" Date: Sun, 24 May 2026 18:59:05 -0700 Subject: [PATCH 4/6] fix --- .github/workflows/build-test-push.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-test-push.yml b/.github/workflows/build-test-push.yml index 83fca75..ff79d39 100644 --- a/.github/workflows/build-test-push.yml +++ b/.github/workflows/build-test-push.yml @@ -161,10 +161,8 @@ jobs: - name: Extract Coverage id: coverage run: | - LINE=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['lineCoverage']))") - BRANCH=$(cat ./coverage/report/Summary.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(round(d['summary']['branchCoverage']))") - echo "line=$LINE" >> $GITHUB_OUTPUT - echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "line=$(jq '.summary.linecoverage' ./coverage/report/Summary.json | xargs printf '%.0f')" >> $GITHUB_OUTPUT + echo "branch=$(jq '.summary.branchcoverage' ./coverage/report/Summary.json | xargs printf '%.0f')" >> $GITHUB_OUTPUT shell: bash # Update line coverage badge in Gist From f319066e43f15b4acd52667494fdb500ee5cb9f7 Mon Sep 17 00:00:00 2001 From: "pi.admin" Date: Mon, 25 May 2026 18:25:44 -0700 Subject: [PATCH 5/6] extensions --- .github/actions/push-package/action.yml | 20 +++++++------- .../Extentions/ObjectExtensions.cs | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 src/OneImlx.Test/Extentions/ObjectExtensions.cs diff --git a/.github/actions/push-package/action.yml b/.github/actions/push-package/action.yml index 580fe0c..862ba5c 100644 --- a/.github/actions/push-package/action.yml +++ b/.github/actions/push-package/action.yml @@ -16,13 +16,13 @@ inputs: description: 'The package version' required: true ghapikey: - description: 'The Git Hub Api Key for package feed' + description: 'The GitHub API Key for package feed' required: true nugetapikey: - description: 'The Nuget Api Key for package feed' + description: 'The NuGet API Key for package feed' required: true registry: - description: 'The package registry (github or nuget)' + description: 'The package registry (GitHub or NuGet)' default: github required: true runs: @@ -47,21 +47,21 @@ runs: dotnet pack ./src/${{ inputs.project }}/${{ inputs.project }}.csproj -p:PackageVersion=${{ inputs.version }} --no-build -c ${{ inputs.config }} -o _packages -v minimal shell: bash - # GITHUB FEED - - name: Github Push + # GitHub Feed + - name: GitHub Push if: ${{ inputs.registry == 'github' }} run: dotnet nuget push _packages/${{ inputs.project }}.${{ inputs.version }}.nupkg --skip-duplicate --no-symbols --api-key ${{ inputs.ghapikey }} --source github shell: bash - # NUGET FEED - # First push to github (for package cadence, Nuget requires package verification) - - name: Github Push + # NuGet Feed + # First push to GitHub (for package cadence, NuGet requires package verification) + - name: GitHub Push if: ${{ inputs.registry == 'nuget' }} run: dotnet nuget push _packages/${{ inputs.project }}.${{ inputs.version }}.nupkg --skip-duplicate --no-symbols --api-key ${{ inputs.ghapikey }} --source github shell: bash - # Second push to Nuget (for package cadence, Github will be used and in production Nuget after package verification) - - name: Nuget Push + # Second push to NuGet (for package cadence, GitHub will be used and in production NuGet after package verification) + - name: NuGet Push if: ${{ inputs.registry == 'nuget' }} run: dotnet nuget push _packages/${{ inputs.project }}.${{ inputs.version }}.nupkg --skip-duplicate --no-symbols --api-key ${{ inputs.nugetapikey }} --source nuget.org shell: bash diff --git a/src/OneImlx.Test/Extentions/ObjectExtensions.cs b/src/OneImlx.Test/Extentions/ObjectExtensions.cs new file mode 100644 index 0000000..5b61f50 --- /dev/null +++ b/src/OneImlx.Test/Extentions/ObjectExtensions.cs @@ -0,0 +1,26 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using FluentAssertions; + +namespace OneImlx.Test.Extentions +{ + /// + /// extension methods for testing. + /// + public static class ObjectExtensions + { + /// + /// Asserts that the source object is not null and returns the source object for chaining. + /// + /// The type of the source object. + /// The source object to check for null. + /// The source object if it is not null. + public static T NotNull(this T? source) + { + source.Should().NotBeNull(); + return source; + } + } +} \ No newline at end of file From 094eeaf1a71e537ef44d26e0a99cfa5ba2348d30 Mon Sep 17 00:00:00 2001 From: "pi.admin" Date: Mon, 25 May 2026 21:19:19 -0700 Subject: [PATCH 6/6] tests for test --- .github/copilot-instructions.md | 4 + .github/workflows/build-test-push.yml | 3 +- Shared.All.Solution.slnx | 1 + .../AssemblyFluentAssertions.cs | 5 +- test/OneImlx.Shared.Tests/AssemblyTests.cs | 9 +- .../Extensions/ObjectExtensionsTests.cs | 42 +++++ .../AssemblyFluentAssertionsTests.cs | 84 ++++++++++ .../ErrorExceptionFluentAssertionsTests.cs | 157 ++++++++++++++++++ .../TypeFluentAssertionsTests.cs | 131 +++++++++++++++ .../OneImlx.Test.Tests.csproj | 8 + 10 files changed, 435 insertions(+), 9 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 test/OneImlx.Test.Tests/Extensions/ObjectExtensionsTests.cs create mode 100644 test/OneImlx.Test.Tests/FluentAssertions/AssemblyFluentAssertionsTests.cs create mode 100644 test/OneImlx.Test.Tests/FluentAssertions/ErrorExceptionFluentAssertionsTests.cs create mode 100644 test/OneImlx.Test.Tests/FluentAssertions/TypeFluentAssertionsTests.cs create mode 100644 test/OneImlx.Test.Tests/OneImlx.Test.Tests.csproj diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..62c46fb --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,4 @@ +# Copilot Instructions + +## Project Guidelines +- Never add section separator comments (e.g., // ── SectionName ──────) in code. \ No newline at end of file diff --git a/.github/workflows/build-test-push.yml b/.github/workflows/build-test-push.yml index ff79d39..e9f2a9e 100644 --- a/.github/workflows/build-test-push.yml +++ b/.github/workflows/build-test-push.yml @@ -139,13 +139,12 @@ jobs: run: echo "PI_CI_REFERENCE=package" >> $GITHUB_ENV continue-on-error: false - # Publish OneImlx.Test (no tests, no coverage) + # Publish OneImlx.Test - name: OneImlx.Test id: oneimlx-test uses: ./.github/actions/push-package with: project: OneImlx.Test - skiptest: 'true' config: Release version: ${{ env.PI_CI_PACKAGE_VERSION }} ghapikey: ${{ env.PI_GITHUB_PAT_ENV }} diff --git a/Shared.All.Solution.slnx b/Shared.All.Solution.slnx index aa761fd..7571cba 100644 --- a/Shared.All.Solution.slnx +++ b/Shared.All.Solution.slnx @@ -13,5 +13,6 @@ + diff --git a/src/OneImlx.Test/FluentAssertions/AssemblyFluentAssertions.cs b/src/OneImlx.Test/FluentAssertions/AssemblyFluentAssertions.cs index 44b9c2a..fb8bd7d 100644 --- a/src/OneImlx.Test/FluentAssertions/AssemblyFluentAssertions.cs +++ b/src/OneImlx.Test/FluentAssertions/AssemblyFluentAssertions.cs @@ -29,7 +29,10 @@ public static AndConstraint HaveTypesInRootNamespace(this As { var assembly = assertions.Subject; var actualNamespace = assembly.GetName().Name ?? throw new InvalidOperationException("Assembly name null"); - actualNamespace.Should().Be(rootNamespace, $"Assembly '{assembly.GetName().Name}' should have root namespace '{rootNamespace}'"); + if (actualNamespace != rootNamespace) + { + throw new AssertionFailedException($"Assembly '{assembly.GetName().Name}' should have root namespace '{rootNamespace}'."); + } var types = assembly.GetTypes().Where(e => !IsCompilerGenerated(e)); var invalidTypes = types.Where(e => diff --git a/test/OneImlx.Shared.Tests/AssemblyTests.cs b/test/OneImlx.Shared.Tests/AssemblyTests.cs index 5eadb4b..368ed3b 100644 --- a/test/OneImlx.Shared.Tests/AssemblyTests.cs +++ b/test/OneImlx.Shared.Tests/AssemblyTests.cs @@ -1,9 +1,6 @@ -/* - Copyright (c) 2023 Perpetual Intelligence L.L.C. All Rights Reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using FluentAssertions; using OneImlx.Test.FluentAssertions; diff --git a/test/OneImlx.Test.Tests/Extensions/ObjectExtensionsTests.cs b/test/OneImlx.Test.Tests/Extensions/ObjectExtensionsTests.cs new file mode 100644 index 0000000..d309f4f --- /dev/null +++ b/test/OneImlx.Test.Tests/Extensions/ObjectExtensionsTests.cs @@ -0,0 +1,42 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using FluentAssertions; +using System; +using Xunit; + +namespace OneImlx.Test.Extentions +{ + public class ObjectExtensionsTests + { + [Fact] + public void NotNullShouldReturnSourceWhenNotNull() + { + string result = "hello".NotNull(); + result.Should().Be("hello"); + } + + [Fact] + public void NotNullShouldThrowWhenNull() + { + string? value = null; + FluentActions.Invoking(() => value.NotNull()).Should().Throw(); + } + + [Fact] + public void NotNullShouldReturnSameReferenceForObject() + { + var obj = new object(); + var result = obj.NotNull(); + result.Should().BeSameAs(obj); + } + + [Fact] + public void NotNullShouldWorkWithValueType() + { + int result = 42.NotNull(); + result.Should().Be(42); + } + } +} \ No newline at end of file diff --git a/test/OneImlx.Test.Tests/FluentAssertions/AssemblyFluentAssertionsTests.cs b/test/OneImlx.Test.Tests/FluentAssertions/AssemblyFluentAssertionsTests.cs new file mode 100644 index 0000000..94e59d8 --- /dev/null +++ b/test/OneImlx.Test.Tests/FluentAssertions/AssemblyFluentAssertionsTests.cs @@ -0,0 +1,84 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using System; +using FluentAssertions; +using FluentAssertions.Execution; +using OneImlx.Shared.Infrastructure; +using Xunit; + +namespace OneImlx.Test.FluentAssertions +{ + public class AssemblyFluentAssertionsTests + { + [Fact] + public void HaveTypesInRootNamespace_PassesWhenAssemblyNameMatchesRootNamespace() + { + var assembly = typeof(AssemblyFluentAssertions).Assembly; + Action act = () => assembly.Should().HaveTypesInRootNamespace("OneImlx.Test"); + act.Should().NotThrow(); + } + + [Fact] + public void HaveTypesInRootNamespace_PassesForNestedNamespacesUnderRoot() + { + var assembly = typeof(AssemblyFluentAssertions).Assembly; + Action act = () => assembly.Should().HaveTypesInRootNamespace("OneImlx.Test"); + act.Should().NotThrow(); + } + + [Fact] + public void HaveTypesInRootNamespace_ThrowsWhenAssemblyNameDoesNotMatchRootNamespace() + { + var assembly = typeof(AssemblyFluentAssertions).Assembly; + Action act = () => assembly.Should().HaveTypesInRootNamespace("WrongNamespace"); + act.Should().Throw().WithMessage("*Assembly 'OneImlx.Test' should have root namespace 'WrongNamespace'*"); + } + + [Fact] + public void HaveTypesInRootNamespace_ThrowsWhenNamespaceIsConcatenationWithoutNesting() + { + // "OneImlx.TestXYZ" starts with "OneImlx.Test" but "XYZ" is not "." — treated as concatenation, not nesting + var assembly = typeof(AssemblyFluentAssertions).Assembly; + Action act = () => assembly.Should().HaveTypesInRootNamespace("OneImlx.TestXYZ"); + act.Should().Throw().WithMessage("*Assembly 'OneImlx.Test' should have root namespace 'OneImlx.TestXYZ'*"); + } + + [Fact] + public void HaveTypesInRootNamespace_ReturnsAndConstraintForChaining() + { + var assembly = typeof(AssemblyFluentAssertions).Assembly; + var result = assembly.Should().HaveTypesInRootNamespace("OneImlx.Test"); + result.Should().NotBeNull(); + } + + [Fact] + public void HaveTypesInValidLocations_PassesForOneImlxTestAssembly() + { + var assembly = typeof(AssemblyFluentAssertions).Assembly; + Action act = () => assembly.Should().HaveTypesInValidLocations(); + act.Should().NotThrow(); + } + + [Fact] + public void HaveTypesInValidLocations_ThrowsWhenExcludedTypeCreatesFilesCountMismatch() + { + // Excluding ObjectExtensions removes the type but the .cs file still exists on disk, + // so type count (0) != file count (1) for namespace OneImlx.Test.Extentions + var excludeTypes = new[] { typeof(Extentions.ObjectExtensions) }; + var assembly = typeof(AssemblyFluentAssertions).Assembly; + Action act = () => assembly.Should().HaveTypesInValidLocations(excludeTypes); + act.Should().Throw() + .WithMessage("Namespace 'OneImlx.Test.Extentions' has 0 types, but it has 1 source files."); + } + + [Fact] + public void HaveTypesInValidLocations_ReturnsAndConstraintForChaining() + { + var assembly = typeof(AssemblyFluentAssertions).Assembly; + var result = assembly.Should().HaveTypesInValidLocations(); + result.Should().NotBeNull(); + } + } +} \ No newline at end of file diff --git a/test/OneImlx.Test.Tests/FluentAssertions/ErrorExceptionFluentAssertionsTests.cs b/test/OneImlx.Test.Tests/FluentAssertions/ErrorExceptionFluentAssertionsTests.cs new file mode 100644 index 0000000..b2bdc50 --- /dev/null +++ b/test/OneImlx.Test.Tests/FluentAssertions/ErrorExceptionFluentAssertionsTests.cs @@ -0,0 +1,157 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using FluentAssertions; +using FluentAssertions.Execution; +using OneImlx.Shared.Infrastructure; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace OneImlx.Test.FluentAssertions +{ + public class ErrorExceptionFluentAssertionsTests + { + [Fact] + public void WithErrorCodeShouldPassWhenErrorCodeMatches() + { + Action act = () => throw new ErrorException(new Error("err_test", "Something went wrong.")); + act.Should().Throw().WithErrorCode("err_test"); + } + + [Fact] + public void WithErrorCodeShouldFailWhenErrorCodeDoesNotMatch() + { + Action act = () => throw new ErrorException(new Error("err_test", "Something went wrong.")); + FluentActions.Invoking(() => + act.Should().Throw().WithErrorCode("wrong_code")) + .Should().Throw(); + } + + [Fact] + public void WithErrorCodeShouldFailWhenExceptionIsNotErrorException() + { + Action act = () => throw new InvalidOperationException("not an ErrorException"); + FluentActions.Invoking(() => + act.Should().Throw().WithErrorCode("err_test")) + .Should().Throw(); + } + + [Fact] + public void WithErrorDescriptionShouldPassWhenDescriptionMatches() + { + Action act = () => throw new ErrorException(new Error("err_test", "Something went wrong.")); + act.Should().Throw().WithErrorDescription("Something went wrong."); + } + + [Fact] + public void WithErrorDescriptionShouldFailWhenDescriptionDoesNotMatch() + { + Action act = () => throw new ErrorException(new Error("err_test", "Something went wrong.")); + FluentActions.Invoking(() => + act.Should().Throw().WithErrorDescription("Wrong description.")) + .Should().Throw(); + } + + [Fact] + public void WithErrorShouldPassWhenErrorMatches() + { + var error = new Error("err_test", "Something went wrong."); + Action act = () => throw new ErrorException(error); + act.Should().Throw().WithError(error); + } + + [Fact] + public void WithErrorShouldFailWhenErrorDoesNotMatch() + { + var error = new Error("err_test", "Something went wrong."); + var differentError = new Error("err_other", "Other error."); + Action act = () => throw new ErrorException(error); + FluentActions.Invoking(() => + act.Should().Throw().WithError(differentError)) + .Should().Throw(); + } + + [Fact] + public async Task WithErrorCodeAsyncShouldPassWhenErrorCodeMatches() + { + Func act = async () => + { + await Task.Yield(); + throw new ErrorException(new Error("err_async", "Async error.")); + }; + + await act.Should().ThrowAsync().WithErrorCode("err_async"); + } + + [Fact] + public async Task WithErrorCodeAsyncShouldFailWhenErrorCodeDoesNotMatch() + { + Func act = async () => + { + await Task.Yield(); + throw new ErrorException(new Error("err_async", "Async error.")); + }; + + await FluentActions.Awaiting(async () => + await act.Should().ThrowAsync().WithErrorCode("wrong_code")) + .Should().ThrowAsync(); + } + + [Fact] + public async Task WithErrorDescriptionAsyncShouldPassWhenDescriptionMatches() + { + Func act = async () => + { + await Task.Yield(); + throw new ErrorException(new Error("err_async", "Async error.")); + }; + + await act.Should().ThrowAsync().WithErrorDescription("Async error."); + } + + [Fact] + public async Task WithErrorDescriptionAsyncShouldFailWhenDescriptionDoesNotMatch() + { + Func act = async () => + { + await Task.Yield(); + throw new ErrorException(new Error("err_async", "Async error.")); + }; + + await FluentActions.Awaiting(async () => + await act.Should().ThrowAsync().WithErrorDescription("Wrong description.")) + .Should().ThrowAsync(); + } + + [Fact] + public async Task WithErrorAsyncShouldPassWhenErrorMatches() + { + var error = new Error("err_async", "Async error."); + Func act = async () => + { + await Task.Yield(); + throw new ErrorException(error); + }; + + await act.Should().ThrowAsync().WithError(error); + } + + [Fact] + public async Task WithErrorAsyncShouldFailWhenErrorDoesNotMatch() + { + var error = new Error("err_async", "Async error."); + var differentError = new Error("err_other", "Other."); + Func act = async () => + { + await Task.Yield(); + throw new ErrorException(error); + }; + + await FluentActions.Awaiting(async () => + await act.Should().ThrowAsync().WithError(differentError)) + .Should().ThrowAsync(); + } + } +} \ No newline at end of file diff --git a/test/OneImlx.Test.Tests/FluentAssertions/TypeFluentAssertionsTests.cs b/test/OneImlx.Test.Tests/FluentAssertions/TypeFluentAssertionsTests.cs new file mode 100644 index 0000000..54c11d1 --- /dev/null +++ b/test/OneImlx.Test.Tests/FluentAssertions/TypeFluentAssertionsTests.cs @@ -0,0 +1,131 @@ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html + +using System; +using System.Text.Json.Serialization; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +namespace OneImlx.Test.FluentAssertions +{ + public class TypeFluentAssertionsTests + { + [Fact] + public void HaveConstantCountShouldPassWhenCountMatches() + { + typeof(SampleWithConstants).Should().HaveConstantCount(2); + } + + [Fact] + public void HaveConstantCountShouldFailWhenCountDoesNotMatch() + { + FluentActions.Invoking(() => typeof(SampleWithConstants).Should().HaveConstantCount(99)) + .Should().Throw(); + } + + [Fact] + public void HavePropertyCountShouldPassWhenCountMatches() + { + typeof(SampleWithProperties).Should().HavePropertyCount(2); + } + + [Fact] + public void HavePropertyCountShouldFailWhenCountDoesNotMatch() + { + FluentActions.Invoking(() => typeof(SampleWithProperties).Should().HavePropertyCount(99)) + .Should().Throw(); + } + + [Fact] + public void HaveJsonPropertyShouldPassWhenAttributeMatches() + { + typeof(SampleWithJsonProps).Should().HaveJsonProperty(nameof(SampleWithJsonProps.FirstName), "first_name"); + } + + [Fact] + public void HaveJsonPropertyShouldFailWhenPropertyMissing() + { + FluentActions.Invoking(() => typeof(SampleWithJsonProps).Should().HaveJsonProperty("NonExistent", "non_existent")) + .Should().Throw(); + } + + [Fact] + public void HaveJsonPropertyShouldFailWhenJsonNameDoesNotMatch() + { + FluentActions.Invoking(() => typeof(SampleWithJsonProps).Should().HaveJsonProperty(nameof(SampleWithJsonProps.FirstName), "wrong_name")) + .Should().Throw(); + } + + [Fact] + public void HaveSnakeCaseJsonNamesShouldPassWhenAllPropertiesDecoratedCorrectly() + { + typeof(SampleWithValidSnakeCase).Should().HaveSnakeCaseJsonNames(); + } + + [Fact] + public void HaveSnakeCaseJsonNamesShouldFailWhenPropertyHasNoAttribute() + { + FluentActions.Invoking(() => typeof(SampleWithMissingAttribute).Should().HaveSnakeCaseJsonNames()) + .Should().Throw(); + } + + [Fact] + public void HaveSnakeCaseJsonNamesShouldFailWhenJsonNameIsNotSnakeCase() + { + FluentActions.Invoking(() => typeof(SampleWithWrongJsonName).Should().HaveSnakeCaseJsonNames()) + .Should().Throw(); + } + + [Fact] + public void HaveSnakeCaseJsonNamesShouldPassWhenPropertyIsJsonIgnored() + { + typeof(SampleWithJsonIgnore).Should().HaveSnakeCaseJsonNames(); + } + + private class SampleWithConstants + { + public const string ConstOne = "one"; + public const string ConstTwo = "two"; + } + + private class SampleWithProperties + { + public string? Name { get; set; } + public int Age { get; set; } + } + + private class SampleWithJsonProps + { + [JsonPropertyName("first_name")] + public string? FirstName { get; set; } + } + + private class SampleWithValidSnakeCase + { + [JsonPropertyName("first_name")] + public string? FirstName { get; set; } + + [JsonPropertyName("last_name")] + public string? LastName { get; set; } + } + + private class SampleWithMissingAttribute + { + public string? FirstName { get; set; } + } + + private class SampleWithWrongJsonName + { + [JsonPropertyName("firstName")] + public string? FirstName { get; set; } + } + + private class SampleWithJsonIgnore + { + [JsonIgnore] + public string? InternalProp { get; set; } + } + } +} \ No newline at end of file diff --git a/test/OneImlx.Test.Tests/OneImlx.Test.Tests.csproj b/test/OneImlx.Test.Tests/OneImlx.Test.Tests.csproj new file mode 100644 index 0000000..dc77032 --- /dev/null +++ b/test/OneImlx.Test.Tests/OneImlx.Test.Tests.csproj @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file