diff --git a/.github/workflows/build-package.yaml b/.github/workflows/build-package.yaml index 2c8632c..5c88f41 100644 --- a/.github/workflows/build-package.yaml +++ b/.github/workflows/build-package.yaml @@ -70,6 +70,11 @@ jobs: name: coverage-report path: artifacts/coverage + # The coverage run is on xunit.v3 3.x. This runs the xUnit tests again on 4.x, against + # DependencyModules.xUnit4. + - name: Test on xunit.v3 4.x + run: ./scripts/test-xunit.sh 4 + # Packs and consumes the packages the way a real user would. Catches packaging faults # that a ProjectReference-based test suite cannot see. - name: Verify packages diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ca1636a..b417d71 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -74,6 +74,9 @@ jobs: cat artifacts/coverage/SummaryGithub.md >> "$GITHUB_STEP_SUMMARY" fi + - name: Test on xunit.v3 4.x + run: ./scripts/test-xunit.sh 4 + - name: Verify packages run: ./scripts/verify-packages.sh "${{ steps.version.outputs.version }}" @@ -86,6 +89,7 @@ jobs: src/DependencyModules.SourceGenerator.Impl/DependencyModules.SourceGenerator.Impl.csproj \ src/DependencyModules.Testing/DependencyModules.Testing.csproj \ src/DependencyModules.xUnit/DependencyModules.xUnit.csproj \ + src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj \ src/DependencyModules.NUnit/DependencyModules.NUnit.csproj \ src/DependencyModules.NSubstitute/DependencyModules.NSubstitute.csproj \ src/DependencyModules.Moq/DependencyModules.Moq.csproj \ diff --git a/.github/workflows/xunit-prerelease.yaml b/.github/workflows/xunit-prerelease.yaml new file mode 100644 index 0000000..477dec1 --- /dev/null +++ b/.github/workflows/xunit-prerelease.yaml @@ -0,0 +1,50 @@ +name: xunit-prerelease + +# Runs the xUnit tests against the newest xunit.v3 on nuget.org, prereleases included. xUnit ships +# prereleases of a major for months before the release, and the 4.0 breaks were in them: the first +# on 2026-04-12, four months before 4.0.0. A failure here is notice of a coming break. Without it, +# the first report comes from a consumer. +on: + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # See build-package.yaml: the 10.0 SDK builds, the 8.0 runtime runs the net8.0 tests. + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - name: Find the newest xunit.v3 and runner + id: versions + run: | + newest() { + curl -fsSL "https://api.nuget.org/v3-flatcontainer/$1/index.json" | python3 -c ' + import json, sys + + def precedence(version): + release, _, prerelease = version.partition("-") + numbers = [int(part) for part in release.split(".")] + if not prerelease: + return (numbers, 1, []) + return (numbers, 0, [(0, int(part), "") if part.isdigit() else (1, 0, part) for part in prerelease.split(".")]) + + print(max(json.load(sys.stdin)["versions"], key=precedence)) + ' + } + echo "xunit=$(newest xunit.v3.mtp-off)" >> "$GITHUB_OUTPUT" + echo "runner=$(newest xunit.runner.visualstudio)" >> "$GITHUB_OUTPUT" + + # 4 is the newest DependencyModules xUnit build. Raise it when a build for a new major is added. + - name: Test on xunit.v3.mtp-off ${{ steps.versions.outputs.xunit }} + run: ./scripts/test-xunit.sh 4 "${{ steps.versions.outputs.xunit }}" "${{ steps.versions.outputs.runner }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f9a9b4..427ef57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,15 @@ To run all tests with code coverage, use the coverage script. The script writes ./scripts/coverage.sh 85 ``` -To do a test of the packages that a user gets, use the package script. The script packs the nine packages. Then it builds and runs a test application for each target framework. The test application references these packages. +The xUnit test projects use `xunit.v3` version 3 and `DependencyModules.xUnit`. To run them with `xunit.v3` version 4 and `DependencyModules.xUnit4`, use the xUnit script: + +```shell +./scripts/test-xunit.sh 4 +``` + +The `XunitMajor` property in `Directory.Build.props` selects the version. The script sets it to the major version that you give. + +To do a test of the packages that a user gets, use the package script. The script packs the ten packages. Then it builds and runs a test application for each target framework. The test application references these packages. ```shell ./scripts/verify-packages.sh @@ -63,7 +71,10 @@ The `build-package` workflow runs for each pull request to `main`. It does these 1. It does a check of the format with CSharpier. 2. It builds the solution. 3. It runs all tests with code coverage. The line coverage must be 85 percent or more. -4. It runs `scripts/verify-packages.sh`. +4. It runs the xUnit tests again with `xunit.v3` version 4. +5. It runs `scripts/verify-packages.sh`. + +The `xunit-prerelease` workflow runs each week. It runs the xUnit tests with the newest `xunit.v3` on nuget.org, prereleases included. A failure tells you about a change in xUnit before its release. ## Documentation @@ -96,8 +107,8 @@ git push origin v1.5.0 The `release` workflow then does these steps: 1. It builds the code. -2. It runs the tests. -3. It packs the nine packages. +2. It runs the tests with `xunit.v3` version 3 and version 4. +3. It packs the ten packages. 4. It publishes the packages to nuget.org and to GitHub Packages. 5. It makes a GitHub release with generated release notes. diff --git a/DependencyModules.sln b/DependencyModules.sln index 19d4acb..61573f4 100644 --- a/DependencyModules.sln +++ b/DependencyModules.sln @@ -46,6 +46,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.NUnit", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SutProject.NUnitTests", "integ-tests\SutProject.NUnitTests\SutProject.NUnitTests.csproj", "{C3B5B8CE-D219-42C8-A1B8-5C283C053E98}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.xUnit4", "src\DependencyModules.xUnit4\DependencyModules.xUnit4.csproj", "{D1AEA1C9-CD76-4664-956B-706962E78E3D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -272,6 +274,18 @@ Global {C3B5B8CE-D219-42C8-A1B8-5C283C053E98}.Release|x64.Build.0 = Release|Any CPU {C3B5B8CE-D219-42C8-A1B8-5C283C053E98}.Release|x86.ActiveCfg = Release|Any CPU {C3B5B8CE-D219-42C8-A1B8-5C283C053E98}.Release|x86.Build.0 = Release|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|x64.Build.0 = Debug|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|x86.Build.0 = Debug|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|Any CPU.Build.0 = Release|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|x64.ActiveCfg = Release|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|x64.Build.0 = Release|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|x86.ActiveCfg = Release|Any CPU + {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -296,5 +310,6 @@ Global {DB6B8603-CBA7-443D-AEA9-E37BF61AD1B7} = {FB161494-E422-4D4E-BA04-3473C2EB13B0} {F911E2B7-4D5A-4DC2-9E3E-4B2CFC30CF4E} = {FB161494-E422-4D4E-BA04-3473C2EB13B0} {C3B5B8CE-D219-42C8-A1B8-5C283C053E98} = {DC7C95FE-F58F-4F16-8DFA-87E1B67DCEAE} + {D1AEA1C9-CD76-4664-956B-706962E78E3D} = {FB161494-E422-4D4E-BA04-3473C2EB13B0} EndGlobalSection EndGlobal diff --git a/Directory.Build.props b/Directory.Build.props index 7799100..f17ff18 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -35,6 +35,31 @@ net8.0;net10.0 + + + 3 + + + + xunit.v3 + 3.2.2 + 3.1.5 + DependencyModules.xUnit + + + + xunit.v3.mtp-off + 4.0.1 + 4.0.0 + DependencyModules.xUnit4 + + Ian Johnson Ian Johnson diff --git a/README.md b/README.md index 27de41f..050a75d 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,10 @@ The `integ-tests` folder contains projects that reference the source projects of | --- | --- | | [DependencyModules.Runtime](https://www.nuget.org/packages/DependencyModules.Runtime/) | The attributes, the interfaces, and the `AddModule` methods. | | [DependencyModules.SourceGenerator](https://www.nuget.org/packages/DependencyModules.SourceGenerator/) | The source generator. It operates only when you compile. | -| [DependencyModules.xUnit](https://www.nuget.org/packages/DependencyModules.xUnit/) | `[ModuleTest]` for xUnit v3. | +| [DependencyModules.xUnit](https://www.nuget.org/packages/DependencyModules.xUnit/) | `[ModuleTest]` for xUnit v3, with `xunit.v3` version 3. | +| [DependencyModules.xUnit4](https://www.nuget.org/packages/DependencyModules.xUnit4/) | `[ModuleTest]` for xUnit v3, with `xunit.v3` version 4. | | [DependencyModules.NUnit](https://www.nuget.org/packages/DependencyModules.NUnit/) | `[ModuleTest]` and `[ModuleTestCase]` for NUnit 4. | -| [DependencyModules.Testing](https://www.nuget.org/packages/DependencyModules.Testing/) | The test attributes and interfaces that the xUnit and NUnit packages use. | +| [DependencyModules.Testing](https://www.nuget.org/packages/DependencyModules.Testing/) | The test attributes and interfaces that the xUnit and NUnit packages use. It also gives the test that runs, and a logger that writes to the output of the test. | | [DependencyModules.NSubstitute](https://www.nuget.org/packages/DependencyModules.NSubstitute/) | `[Mock]` parameters with NSubstitute. | | [DependencyModules.Moq](https://www.nuget.org/packages/DependencyModules.Moq/) | `[Mock]` and `Mock` parameters with Moq. | | [DependencyModules.FakeItEasy](https://www.nuget.org/packages/DependencyModules.FakeItEasy/) | `[Mock]` parameters with FakeItEasy. | diff --git a/integ-tests/SutProject.NUnitTests/CurrentTestTests.cs b/integ-tests/SutProject.NUnitTests/CurrentTestTests.cs new file mode 100644 index 0000000..3dd02eb --- /dev/null +++ b/integ-tests/SutProject.NUnitTests/CurrentTestTests.cs @@ -0,0 +1,101 @@ +using DependencyModules.NUnit.Attributes; +using DependencyModules.Testing.Attributes.Interfaces; +using DependencyModules.Testing.Impl; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using NUnit.Framework.Internal; + +namespace SutProject.NUnitTests; + +/// +/// CurrentTest under NUnit, where the runner package answers from +/// TestExecutionContext.CurrentContext. +/// +public class CurrentTestTests +{ + private static readonly List RepeatKeys = []; + + [ModuleTest] + public void KeyIsNUnitsObjectForTheRunningTest() => + Assert.That(CurrentTest.Key, Is.SameAs(TestExecutionContext.CurrentContext.CurrentTest)); + + [ModuleTest] + public void DisplayNameIsTheFullNameOfTheTest() => + Assert.That(CurrentTest.DisplayName, Is.EqualTo(TestContext.CurrentContext.Test.FullName)); + + [ModuleTest] + public void AssemblyIsTheAssemblyOfTheTestClass() => + Assert.That(CurrentTest.Assembly, Is.SameAs(typeof(CurrentTestTests).Assembly)); + + [ModuleTest] + public void ALineIsWrittenToTheOutputOfTheTest() + { + Assert.That(CurrentTest.TryWriteLine("written through CurrentTest"), Is.True); + + Assert.That( + TestExecutionContext.CurrentContext.CurrentResult.Output, + Does.Contain("written through CurrentTest") + ); + } + + [ModuleTest] + public void TheLoggerProviderWritesToTheOutputOfTheTest() + { + new TestOutputLoggerProvider() + .CreateLogger("Shop.OrderService") + .LogInformation("Order {Id} accepted", 42); + + Assert.That( + TestExecutionContext.CurrentContext.CurrentResult.Output, + Does.Contain("info: Shop.OrderService[0] Order 42 accepted") + ); + } + + /// + /// The container is built inside the iteration, so the test is already running in the hooks + /// that build it. Under xUnit it is not; the documentation of CurrentTest gives both. + /// + [ModuleTest] + [RecordCurrentTestAtStartup] + public void TheTestIsRunningWhileTheContainerIsBuilt() + { + Assert.That(RecordCurrentTestAtStartupAttribute.Key, Is.SameAs(CurrentTest.Key)); + Assert.That( + TestExecutionContext.CurrentContext.CurrentResult.Output, + Does.Contain("written while the container is built") + ); + } + + /// + /// NUnit makes no object of its own for one iteration, so the key is the test's for all of + /// them. The documentation of CurrentTest says so; the container is still one per iteration. + /// + [ModuleTest] + [Repeat(2)] + public void TheIterationsOfARepeatedTestShareOneKey() + { + RepeatKeys.Add(CurrentTest.Key); + + Assert.That(CurrentTest.Key, Is.Not.Null); + Assert.That( + RepeatKeys, + Has.Count.EqualTo(TestExecutionContext.CurrentContext.CurrentRepeatCount + 1) + ); + Assert.That(RepeatKeys, Is.All.SameAs(RepeatKeys[0])); + } +} + +[AttributeUsage(AttributeTargets.Method)] +public sealed class RecordCurrentTestAtStartupAttribute : Attribute, ITestStartupAttribute +{ + public static object? Key { get; private set; } + + public Task StartupAsync(ITestMethodContext testMethod, IServiceProvider serviceProvider) + { + Key = CurrentTest.Key; + + CurrentTest.TryWriteLine("written while the container is built"); + + return Task.CompletedTask; + } +} diff --git a/integ-tests/SutProject.Tests/SutProject.Tests.csproj b/integ-tests/SutProject.Tests/SutProject.Tests.csproj index f341eec..dcda87a 100644 --- a/integ-tests/SutProject.Tests/SutProject.Tests.csproj +++ b/integ-tests/SutProject.Tests/SutProject.Tests.csproj @@ -12,7 +12,7 @@ - + @@ -24,11 +24,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/integ-tests/SutProject.Tests/TestFramework/CurrentTestTests.cs b/integ-tests/SutProject.Tests/TestFramework/CurrentTestTests.cs new file mode 100644 index 0000000..06d3e92 --- /dev/null +++ b/integ-tests/SutProject.Tests/TestFramework/CurrentTestTests.cs @@ -0,0 +1,167 @@ +using System.Reflection; +using DependencyModules.Testing.Attributes.Interfaces; +using DependencyModules.Testing.Impl; +using DependencyModules.xUnit.Attributes; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace SutProject.Tests.TestFramework; + +/// +/// CurrentTest under xUnit, where the runner package answers from TestContext.Current. +/// +/// +/// SutProject.Tests runs against both xunit.v3 majors, so these hold for DependencyModules.xUnit and +/// for DependencyModules.xUnit4. +/// +public class CurrentTestTests +{ + private static readonly Dictionary RowKeys = []; + + [ModuleTest] + public void KeyIsXunitsObjectForTheRunningTest() => + Assert.Same(TestContext.Current.Test, CurrentTest.Key); + + [ModuleTest] + public void DisplayNameIsXunitsDisplayName() => + Assert.Equal(TestContext.Current.Test!.TestDisplayName, CurrentTest.DisplayName); + + [ModuleTest] + public void AssemblyIsTheAssemblyOfTheTestClass() => + Assert.Same(typeof(CurrentTestTests).Assembly, CurrentTest.Assembly); + + [ModuleTest] + public void ALineIsWrittenToTheOutputOfTheTest() + { + Assert.True(CurrentTest.TryWriteLine("written through CurrentTest")); + + Assert.Contains( + "written through CurrentTest", + TestContext.Current.TestOutputHelper!.Output + ); + } + + [ModuleTest] + public void TheLoggerProviderWritesToTheOutputOfTheTest() + { + new TestOutputLoggerProvider() + .CreateLogger("Shop.OrderService") + .LogInformation("Order {Id} accepted", 42); + + Assert.Contains( + "info: Shop.OrderService[0] Order 42 accepted", + TestContext.Current.TestOutputHelper!.Output + ); + } + + /// + /// Each row is a test of its own, so per-test state keyed on one row does not reach another. + /// + [ModuleTest] + [InlineData(1)] + [InlineData(2)] + public void EachDataRowHasAKeyOfItsOwn(int row) + { + var key = Assert.IsAssignableFrom(CurrentTest.Key); + + lock (RowKeys) + { + Assert.DoesNotContain(RowKeys.Values, earlier => ReferenceEquals(earlier, key)); + + RowKeys[row] = key; + } + } + + /// + /// A background flow keeps the context of the test that started it after that test finishes, + /// and xUnit's output helper then throws when written to. A logger on such a flow must drop + /// the line and not throw into the application under test. + /// + /// + /// Two tests in one class, which xUnit runs one after the other in an order it does not + /// promise. Whichever runs first starts the writer. The second tells it that the first has + /// finished, and reads what the write returned. + /// + [ModuleTest] + public Task ALineWrittenAfterItsTestHasFinishedIsDropped() => WriteAfterTheTestAsync(); + + [ModuleTest] + public Task WhicheverOfTheTwoLateWriteTestsRanFirst() => WriteAfterTheTestAsync(); + + private static readonly TaskCompletionSource FirstLateWriteTestFinished = new( + TaskCreationOptions.RunContinuationsAsynchronously + ); + + private static readonly TaskCompletionSource LateWrite = new( + TaskCreationOptions.RunContinuationsAsynchronously + ); + + private static int _lateWriteTestsStarted; + + private static async Task WriteAfterTheTestAsync() + { + if (Interlocked.Increment(ref _lateWriteTestsStarted) == 1) + { + _ = Task.Run(async () => + { + await FirstLateWriteTestFinished.Task; + + try + { + LateWrite.SetResult( + CurrentTest.TryWriteLine("written after the test finished") + ); + } + catch (Exception exception) + { + LateWrite.SetException(exception); + } + }); + + return; + } + + FirstLateWriteTestFinished.SetResult(); + + Assert.False(await LateWrite.Task.WaitAsync(TimeSpan.FromSeconds(10))); + } + + /// + /// A case builds the container for each of its tests before xUnit runs the first one, so no + /// test is running yet in the hooks that build it. The documentation of CurrentTest says so. + /// + [ModuleTest] + [RecordCurrentTestAtStartup] + public void NoTestIsRunningWhileTheContainerIsBuilt() + { + Assert.True(RecordCurrentTestAtStartupAttribute.Recorded); + Assert.Null(RecordCurrentTestAtStartupAttribute.Key); + Assert.False(RecordCurrentTestAtStartupAttribute.Wrote); + Assert.Same( + typeof(CurrentTestTests).Assembly, + RecordCurrentTestAtStartupAttribute.Assembly + ); + } +} + +[AttributeUsage(AttributeTargets.Method)] +public sealed class RecordCurrentTestAtStartupAttribute : Attribute, ITestStartupAttribute +{ + public static bool Recorded { get; private set; } + + public static object? Key { get; private set; } + + public static Assembly? Assembly { get; private set; } + + public static bool Wrote { get; private set; } + + public Task StartupAsync(ITestMethodContext testMethod, IServiceProvider serviceProvider) + { + Key = CurrentTest.Key; + Assembly = CurrentTest.Assembly; + Wrote = CurrentTest.TryWriteLine("written while the container is built"); + Recorded = true; + + return Task.CompletedTask; + } +} diff --git a/integ-tests/web/WebApiApp.Tests/WebApiApp.Tests.csproj b/integ-tests/web/WebApiApp.Tests/WebApiApp.Tests.csproj index fa0c3bb..8c9c0f0 100644 --- a/integ-tests/web/WebApiApp.Tests/WebApiApp.Tests.csproj +++ b/integ-tests/web/WebApiApp.Tests/WebApiApp.Tests.csproj @@ -9,7 +9,7 @@ - + @@ -20,10 +20,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/scripts/test-xunit.sh b/scripts/test-xunit.sh new file mode 100755 index 0000000..4fed26c --- /dev/null +++ b/scripts/test-xunit.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# Runs the xUnit test projects against one xunit.v3 major. The default build and the coverage +# script use major 3. CI runs this for 4. +# +# Usage: +# scripts/test-xunit.sh 4 the versions Directory.Build.props sets for 4 +# scripts/test-xunit.sh 4 4.1.0-pre.12 4.0.0 a given xunit.v3.mtp-off and runner version + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MAJOR="${1:?usage: scripts/test-xunit.sh [xunit.v3 version] [runner version]}" + +PROPERTIES=("-p:XunitMajor=${MAJOR}") +if [ -n "${2:-}" ]; then PROPERTIES+=("-p:XunitV3Version=$2"); fi +if [ -n "${3:-}" ]; then PROPERTIES+=("-p:XunitRunnerVersion=$3"); fi + +PROJECTS=( + "tests/DependencyModules.Tests/DependencyModules.Tests.csproj" + "integ-tests/SutProject.Tests/SutProject.Tests.csproj" + "integ-tests/web/WebApiApp.Tests/WebApiApp.Tests.csproj" +) + +TEST_LOG="$(mktemp)" +trap 'rm -f "${TEST_LOG}"' EXIT + +for project in "${PROJECTS[@]}"; do + echo "==> ${project} (xunit.v3 major ${MAJOR})" + dotnet test "${REPO_ROOT}/${project}" \ + --configuration Release \ + --nologo \ + "${PROPERTIES[@]}" | tee -a "${TEST_LOG}" + + # tee masks the exit status, so consult the pipeline's first element. + status="${PIPESTATUS[0]}" + [ "${status}" -eq 0 ] || exit "${status}" +done + +# The same guard as scripts/coverage.sh: xUnit drops a test case whose unique ID collides with +# one already discovered, and the run stays green. +if grep -q "duplicate ID" "${TEST_LOG}"; then + echo >&2 + echo "FAIL: xUnit skipped a test case with a duplicate unique ID. Tests were silently dropped." >&2 + grep "duplicate ID" "${TEST_LOG}" >&2 + exit 1 +fi diff --git a/scripts/verify-packages.sh b/scripts/verify-packages.sh index 5f21639..ba3327a 100755 --- a/scripts/verify-packages.sh +++ b/scripts/verify-packages.sh @@ -60,6 +60,7 @@ for proj in \ src/DependencyModules.SourceGenerator.Impl/DependencyModules.SourceGenerator.Impl.csproj \ src/DependencyModules.Testing/DependencyModules.Testing.csproj \ src/DependencyModules.xUnit/DependencyModules.xUnit.csproj \ + src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj \ src/DependencyModules.NUnit/DependencyModules.NUnit.csproj \ src/DependencyModules.NSubstitute/DependencyModules.NSubstitute.csproj \ src/DependencyModules.Moq/DependencyModules.Moq.csproj \ @@ -98,6 +99,7 @@ LIB_PACKAGES=( DependencyModules.Runtime DependencyModules.Testing DependencyModules.xUnit + DependencyModules.xUnit4 DependencyModules.NUnit DependencyModules.NSubstitute DependencyModules.Moq @@ -131,6 +133,29 @@ for tfm, body in re.findall(r"(.*?)" pass "${id} dependency groups are framework-matched" done +# A test package is built against one major of its framework and must stop below the next. A bare +# version packs as an open range, and NuGet then unifies a test project's newer major into it, +# which loads nothing at run time and warns about nothing at restore. +for pair in \ + DependencyModules.xUnit:xunit.v3.extensibility.core \ + DependencyModules.xUnit4:xunit.v3.extensibility.core \ + DependencyModules.NUnit:NUnit; do + id="${pair%%:*}" + dependency="${pair#*:}" + unzip -p "${FEED}/${id}.${VERSION}.nupkg" "${id}.nuspec" | python3 -c ' +import re, sys +dependency = sys.argv[1] +ranges = set(re.findall(r"id=\"" + re.escape(dependency) + r"\" version=\"([^\"]+)\"", sys.stdin.read())) +if not ranges: + sys.exit(f" no dependency on {dependency}") +for value in ranges: + match = re.fullmatch(r"\[(\d+)\.\d+\.\d+, (\d+)\.0\.0\)", value) + if not match or int(match.group(2)) != int(match.group(1)) + 1: + sys.exit(f" {dependency} is {value}, expected [lowest, next major.0.0)") +' "${dependency}" || fail "${id}: its ${dependency} range does not stop below the next major" + pass "${id} stops ${dependency} below the next major" +done + # Placeholder metadata must never ship. for pkg in "${FEED}"/*.nupkg; do id="$(basename "${pkg}" ".${VERSION}.nupkg")" diff --git a/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs b/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs index 0286a9a..53a2097 100644 --- a/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs +++ b/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs @@ -57,6 +57,9 @@ public class ModuleTestAttribute /// internal const string RowPropertyName = "DependencyModules.ModuleTestRow"; + // NUnit creates the attribute while it builds the tests, before the first one runs. + static ModuleTestAttribute() => NUnitCurrentTestProvider.Install(); + /// /// Marks a test method, optionally naming the modules to configure its container with. /// diff --git a/src/DependencyModules.NUnit/Impl/NUnitCurrentTestProvider.cs b/src/DependencyModules.NUnit/Impl/NUnitCurrentTestProvider.cs new file mode 100644 index 0000000..2ea14ad --- /dev/null +++ b/src/DependencyModules.NUnit/Impl/NUnitCurrentTestProvider.cs @@ -0,0 +1,50 @@ +using System.Reflection; +using DependencyModules.Testing.Attributes.Interfaces; +using DependencyModules.Testing.Impl; +using NUnit.Framework; +using NUnit.Framework.Internal; + +namespace DependencyModules.NUnit.Impl; + +/// +/// Reads the running test from NUnit's . +/// +/// +/// +/// Installed from the static constructor of ModuleTestAttribute. NUnit creates the attribute +/// while it builds the tests, before the first one runs. +/// +/// +/// The key is NUnit's , which the iterations of a [Repeat] or a +/// [Retry] share: NUnit makes no object of its own for one iteration. The container is still +/// built for each iteration. +/// +/// +internal sealed class NUnitCurrentTestProvider : ICurrentTestProvider +{ + public object? Key => RunningTest; + + public string? DisplayName => RunningTest?.FullName; + + public Assembly? Assembly => RunningTest?.TypeInfo?.Assembly; + + public bool TryWriteLine(string message) + { + if (RunningTest == null) + { + return false; + } + + TestContext.Out.WriteLine(message); + + return true; + } + + public static void Install() => CurrentTest.Provider ??= new NUnitCurrentTestProvider(); + + // NUnit answers outside a test with its ad hoc context rather than with null. + private static Test? RunningTest => + TestExecutionContext.CurrentContext is TestExecutionContext.AdhocContext + ? null + : TestExecutionContext.CurrentContext.CurrentTest; +} diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/ICurrentTestProvider.cs b/src/DependencyModules.Testing/Attributes/Interfaces/ICurrentTestProvider.cs new file mode 100644 index 0000000..8739f33 --- /dev/null +++ b/src/DependencyModules.Testing/Attributes/Interfaces/ICurrentTestProvider.cs @@ -0,0 +1,51 @@ +using System.Reflection; + +namespace DependencyModules.Testing.Attributes.Interfaces; + +/// +/// Reads the running test from the test framework's own context. +/// +/// +/// +/// The ambient counterpart of . That one is handed to the hooks that +/// build a test's container. This one answers from anywhere the framework's context flows, such as +/// an assertion helper or a logger, with nothing handed to it. Each runner package installs its own +/// in CurrentTest.Provider: DependencyModules.xUnit and DependencyModules.xUnit4 read +/// TestContext.Current, and DependencyModules.NUnit reads +/// TestExecutionContext.CurrentContext. +/// +/// +/// Every member answers for the test running on the calling flow, and with null or false when there +/// is none. A library that reads this works under any runner package and references no framework. +/// +/// +public interface ICurrentTestProvider +{ + /// + /// The framework's own object for the running test, or null when no test is running. + /// + /// + /// Compared by reference. Two tests that run at the same time have different keys, and the + /// framework holds the object for as long as the test runs, so a weak table can hold per-test + /// state against it. + /// + object? Key { get; } + + /// + /// The framework's display name for the running test, or null when no test is running. + /// + string? DisplayName { get; } + + /// + /// The assembly that declares the running test's class, or null when there is no test class. + /// + Assembly? Assembly { get; } + + /// + /// Writes a line to the running test's output. + /// + /// + /// False, having written nothing, when no test is running or the test has finished. + /// + bool TryWriteLine(string message); +} diff --git a/src/DependencyModules.Testing/DependencyModules.Testing.csproj b/src/DependencyModules.Testing/DependencyModules.Testing.csproj index 3ff2aa8..049b4d6 100644 --- a/src/DependencyModules.Testing/DependencyModules.Testing.csproj +++ b/src/DependencyModules.Testing/DependencyModules.Testing.csproj @@ -6,17 +6,19 @@ enable True DependencyModules.Testing - The test attributes and interfaces of DependencyModules: [Mock], [InjectValues], [Shared], [TestExport], and the interfaces for mock libraries and test attributes. It also contains the helpers that find the attributes of a test on the method, the class, and the assembly. It does not depend on a test framework. The DependencyModules.xUnit and DependencyModules.NUnit packages use this package. Add one of those packages to a test project. + The test attributes and interfaces of DependencyModules: [Mock], [InjectValues], [Shared], [TestExport], and the interfaces for mock libraries and test attributes. It also contains the helpers that find the attributes of a test on the method, the class, and the assembly. CurrentTest gives the test that runs. TestOutputLoggerProvider writes log entries to the output of the test that runs. It does not depend on a test framework. The DependencyModules.xUnit, DependencyModules.xUnit4, and DependencyModules.NUnit packages use this package. Add one of those packages to a test project. true + + diff --git a/src/DependencyModules.Testing/Impl/CurrentTest.cs b/src/DependencyModules.Testing/Impl/CurrentTest.cs new file mode 100644 index 0000000..9960a22 --- /dev/null +++ b/src/DependencyModules.Testing/Impl/CurrentTest.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using DependencyModules.Testing.Attributes.Interfaces; + +namespace DependencyModules.Testing.Impl; + +/// +/// The running test, read through the that the runner package +/// installed. +/// +/// +/// +/// DependencyModules.xUnit, DependencyModules.xUnit4 and DependencyModules.NUnit install their +/// provider before the first of their tests runs. Without a runner package is +/// null, and every member answers as if no test were running. +/// +/// +/// Settable, so that a test of code which reads this can put a provider of its own in place. A runner +/// installs its provider only when none is set. +/// +/// +public static class CurrentTest +{ + /// + /// The provider every member reads through, or null when no runner package has installed one. + /// + public static ICurrentTestProvider? Provider { get; set; } + + /// + public static object? Key => Provider?.Key; + + /// + public static string? DisplayName => Provider?.DisplayName; + + /// + public static Assembly? Assembly => Provider?.Assembly; + + /// + public static bool TryWriteLine(string message) => Provider?.TryWriteLine(message) ?? false; +} diff --git a/src/DependencyModules.Testing/Impl/TestOutputLoggerProvider.cs b/src/DependencyModules.Testing/Impl/TestOutputLoggerProvider.cs new file mode 100644 index 0000000..0b4b7f2 --- /dev/null +++ b/src/DependencyModules.Testing/Impl/TestOutputLoggerProvider.cs @@ -0,0 +1,119 @@ +using System.Collections.Concurrent; +using System.Text; +using DependencyModules.Testing.Attributes.Interfaces; +using Microsoft.Extensions.Logging; + +namespace DependencyModules.Testing.Impl; + +/// +/// A logger provider that writes each entry to the output of the running test. +/// +/// +/// +/// An entry is one line in the console logger's single-line format, +/// info: Shop.OrderService[0] Order 42 accepted, with an exception on the lines after it. +/// An entry logged while no test is running is dropped, because there is no output to write it to. +/// +/// +/// Registered in a test's container as an , it puts the log of the +/// application under test into the test's output. The container's logger factory writes to it +/// beside any other provider. +/// +/// +public sealed class TestOutputLoggerProvider : ILoggerProvider +{ + private readonly ICurrentTestProvider? _currentTest; + + private readonly ConcurrentDictionary _loggers = new( + StringComparer.Ordinal + ); + + /// + /// Writes through , read again for each entry. + /// + public TestOutputLoggerProvider() { } + + /// + /// Writes through . + /// + public TestOutputLoggerProvider(ICurrentTestProvider currentTest) + { + ArgumentNullException.ThrowIfNull(currentTest); + + _currentTest = currentTest; + } + + /// + public ILogger CreateLogger(string categoryName) => + _loggers.GetOrAdd(categoryName, name => new TestOutputLogger(name, this)); + + /// + public void Dispose() { } + + private void Write(string entry) => (_currentTest ?? CurrentTest.Provider)?.TryWriteLine(entry); + + private sealed class TestOutputLogger(string categoryName, TestOutputLoggerProvider provider) + : ILogger + { + public IDisposable? BeginScope(TState state) + where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter + ) + { + ArgumentNullException.ThrowIfNull(formatter); + + if (!IsEnabled(logLevel)) + { + return; + } + + var message = formatter(state, exception); + + // What the console logger does with an entry that has nothing to show. + if (string.IsNullOrEmpty(message) && exception == null) + { + return; + } + + var entry = new StringBuilder() + .Append(LevelName(logLevel)) + .Append(": ") + .Append(categoryName) + .Append('[') + .Append(eventId.Id) + .Append(']'); + + if (!string.IsNullOrEmpty(message)) + { + entry.Append(' ').Append(message); + } + + if (exception != null) + { + entry.AppendLine().Append(exception); + } + + provider.Write(entry.ToString()); + } + + private static string LevelName(LogLevel logLevel) => + logLevel switch + { + LogLevel.Trace => "trce", + LogLevel.Debug => "dbug", + LogLevel.Information => "info", + LogLevel.Warning => "warn", + LogLevel.Error => "fail", + LogLevel.Critical => "crit", + _ => logLevel.ToString(), + }; + } +} diff --git a/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs b/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs index d76a20c..2c8c444 100644 --- a/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs +++ b/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs @@ -23,6 +23,9 @@ namespace DependencyModules.xUnit.Attributes; [AttributeUsage(AttributeTargets.Method)] public class ModuleTestAttribute : FactAttribute, IModuleTestAttribute { + // xUnit creates the attribute while it discovers the tests, before the first one runs. + static ModuleTestAttribute() => XunitCurrentTestProvider.Install(); + /// /// Marks a test method, taking no modules. /// diff --git a/src/DependencyModules.xUnit/DependencyModules.xUnit.csproj b/src/DependencyModules.xUnit/DependencyModules.xUnit.csproj index c54c3b0..80cefae 100644 --- a/src/DependencyModules.xUnit/DependencyModules.xUnit.csproj +++ b/src/DependencyModules.xUnit/DependencyModules.xUnit.csproj @@ -6,7 +6,7 @@ enable True DependencyModules.xUnit - xUnit v3 support for DependencyModules. [ModuleTest] builds a service provider from your modules for each test and each data row. The test parameters get their values from this service provider. + xUnit v3 support for DependencyModules, for xunit.v3 version 3. [ModuleTest] builds a service provider from your modules for each test and each data row. The test parameters get their values from this service provider. For xunit.v3 version 4, use DependencyModules.xUnit4. true @@ -22,17 +22,20 @@ diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestCase.XunitMajor.cs b/src/DependencyModules.xUnit/Impl/ModuleTestCase.XunitMajor.cs new file mode 100644 index 0000000..f502ed2 --- /dev/null +++ b/src/DependencyModules.xUnit/Impl/ModuleTestCase.XunitMajor.cs @@ -0,0 +1,158 @@ +using Xunit.Sdk; +using Xunit.v3; + +namespace DependencyModules.xUnit.Impl; + +// The members of ModuleTestCase whose xUnit signatures differ between xunit.v3 majors, and nothing +// else. DependencyModules.xUnit compiles this file against 3.x. DependencyModules.xUnit4 compiles the +// same sources against 4.x with XUNIT_V4 defined. A later major adds a branch here and a project of +// its own. +public partial class ModuleTestCase +{ +#if XUNIT_V4 + /// + /// Runs the case the way xUnit would have, and disposes every container it built once the + /// run has returned - the tests passed, failed, were skipped or were cancelled alike. + /// + /// + /// XunitRunnerHelper.RunXunitTestCase is what the method runner calls for a case that + /// does not execute itself: it creates the tests, turns a failure or a dynamic skip during + /// creation into the case's result, and hands the tests to . + /// Wrapping that call is the whole of the difference. Disposal is per case, which for every + /// test but a data-driven one is per test; the rows of a data-driven test share the case and + /// are released together when the last has run. + /// + public async ValueTask Run( + ExplicitOption explicitOption, + IMessageBus messageBus, + object?[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource, + ParallelMode parallelMode, + ExecutionScheduler scheduler, + FixtureMappingManager methodFixtureMappings + ) + { + try + { + return await XunitRunnerHelper.RunXunitTestCase( + testCase: this, + messageBus: messageBus, + cancellationTokenSource: cancellationTokenSource, + parallelMode: parallelMode, + scheduler: scheduler, + aggregator: aggregator, + explicitOption: explicitOption, + constructorArguments: constructorArguments, + methodFixtureMappings: methodFixtureMappings + ); + } + finally + { + await DisposeProviders(); + } + } +#else + /// + /// Runs the case the way xUnit would have, and disposes every container it built once the + /// run has returned - the tests passed, failed, were skipped or were cancelled alike. + /// + /// + /// is what the method runner calls for a + /// case that does not execute itself: it creates the tests, turns a failure or a dynamic skip + /// during creation into the case's result, and hands the tests to + /// . Wrapping that call is the whole of the difference. + /// Disposal is per case, which for every test but a data-driven one is per test; the rows of + /// a data-driven test share the case and are released together when the last has run. + /// + public async ValueTask Run( + ExplicitOption explicitOption, + IMessageBus messageBus, + object?[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource + ) + { + try + { + return await XunitRunnerHelper.RunXunitTestCase( + testCase: this, + messageBus: messageBus, + cancellationTokenSource: cancellationTokenSource, + aggregator: aggregator, + explicitOption: explicitOption, + constructorArguments: constructorArguments + ); + } + finally + { + await DisposeProviders(); + } + } +#endif + + /// + /// Creates one test of this case from values already resolved against the case. + /// + /// + /// and are the data row the test comes + /// from and the attribute that gave it, or null for a test without data. 4.x reads a label and + /// a parallelization choice from them, which 3.x does not have. + /// + /// Named throughout: has a second constructor that takes a uniqueID + /// string near where this passes testIndex, and positionally the two are told apart by the + /// argument's type alone. + /// + private XunitTest CreateTest( + string? skipReason, + Type? skipType, + string? skipUnless, + string? skipWhen, + string testDisplayName, + int testIndex, + IReadOnlyDictionary> traits, + int timeout, + object?[] testMethodArguments, + Xunit.ITheoryDataRow? row, + IDataAttribute? dataAttribute + ) + { +#if XUNIT_V4 + return new XunitTest( + testCase: this, + testMethod: TestMethod, + @explicit: Explicit, + skipReason: skipReason, + skipType: skipType, + skipUnless: skipUnless, + skipWhen: skipWhen, + testDisplayName: testDisplayName, + testIndex: testIndex, + traits: traits, + timeout: timeout, + testMethodArguments: testMethodArguments, + // What [Theory] passes for a row and [Fact] for a test without data. Left to the + // obsolete constructor, a row marked DisableParallelization would run in parallel. + testLabel: row?.Label ?? TestLabel, + disableParallelization: row?.DisableParallelization + ?? dataAttribute?.DisableParallelization + ?? DisableParallelization + ); +#else + return new XunitTest( + testCase: this, + testMethod: TestMethod, + @explicit: Explicit, + skipReason: skipReason, + skipType: skipType, + skipUnless: skipUnless, + skipWhen: skipWhen, + testDisplayName: testDisplayName, + testIndex: testIndex, + traits: traits, + timeout: timeout, + testMethodArguments: testMethodArguments + ); +#endif + } +} diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs b/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs index 461e862..cf0fd70 100644 --- a/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs +++ b/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs @@ -22,7 +22,7 @@ namespace DependencyModules.xUnit.Impl; /// ModuleTestCommand has always disposed in a finally around the test; this is the /// same lifetime for xUnit. /// -public class ModuleTestCase : XunitTestCase, ISelfExecutingXunitTestCase +public partial class ModuleTestCase : XunitTestCase, ISelfExecutingXunitTestCase { /// /// One per container this case built: one for a plain test, one per row for a data-driven @@ -30,6 +30,10 @@ public class ModuleTestCase : XunitTestCase, ISelfExecutingXunitTestCase /// private readonly List _providers = []; + // Here as well as on ModuleTestAttribute, because an attribute of another library can name + // ModuleTestDiscoverer, and then this is the first of the package's types that xUnit creates. + static ModuleTestCase() => XunitCurrentTestProvider.Install(); + #pragma warning disable CS0618 // Type or member is obsolete /// /// Represents a specialized implementation of @@ -292,43 +296,6 @@ IEnumerable knownAttributes DependencyRegistry.LoadModules(serviceCollection, modules.ToArray()); } - /// - /// Runs the case the way xUnit would have, and disposes every container it built once the - /// run has returned - the tests passed, failed, were skipped or were cancelled alike. - /// - /// - /// is what the method runner calls for a - /// case that does not execute itself: it creates the tests, turns a failure or a dynamic skip - /// during creation into the case's result, and hands the tests to - /// . Wrapping that call is the whole of the difference. - /// Disposal is per case, which for every test but a data-driven one is per test; the rows of - /// a data-driven test share the case and are released together when the last has run. - /// - public async ValueTask Run( - ExplicitOption explicitOption, - IMessageBus messageBus, - object?[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource - ) - { - try - { - return await XunitRunnerHelper.RunXunitTestCase( - this, - messageBus, - cancellationTokenSource, - aggregator, - explicitOption, - constructorArguments - ); - } - finally - { - await DisposeProviders(); - } - } - private async ValueTask DisposeProviders() { var providers = _providers.ToArray(); @@ -419,13 +386,7 @@ IDataAttribute[] dataAttributes var startupValues = await SetupServiceCollection(); unitTests.Add( - // testIndex is named for more than readability: XunitTest has a second - // nine-parameter constructor differing only at this position, taking a uniqueID - // string. Positionally the two are told apart by the argument's type alone. - new XunitTest( - testCase: this, - testMethod: TestMethod, - @explicit: Explicit, + CreateTest( skipReason: theoryDataRow.Skip ?? SkipReason, // The row's own conditional-skip metadata takes precedence over the case's, // matching how skipReason above already defers to it. These became required @@ -442,7 +403,9 @@ IDataAttribute[] dataAttributes .GetTraits(TestMethod, theoryDataRow) .ToReadOnlyTraits(), timeout: theoryDataRow.Timeout ?? Timeout, - testMethodArguments: await ResolveArguments(data, startupValues) + testMethodArguments: await ResolveArguments(data, startupValues), + row: theoryDataRow, + dataAttribute: dataAttribute ) ); } @@ -513,10 +476,7 @@ private async Task> UnitTestWithNoDataAttributes return [ - new XunitTest( - testCase: this, - testMethod: TestMethod, - @explicit: Explicit, + CreateTest( skipReason: SkipReason, skipType: SkipType, skipUnless: SkipUnless, @@ -525,7 +485,9 @@ private async Task> UnitTestWithNoDataAttributes testIndex: 0, traits: Traits.ToReadOnlyTraits(), timeout: Timeout, - testMethodArguments: await ResolveArguments([], startupValues) + testMethodArguments: await ResolveArguments([], startupValues), + row: null, + dataAttribute: null ), ]; } diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs b/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs index f49adad..c33d44d 100644 --- a/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs +++ b/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs @@ -47,7 +47,8 @@ IFactAttribute factAttribute // second GetTestCaseDetails taking a trailing label, and since every added parameter on // both is optional, a three-argument call matches the two equally well and is ambiguous. // Naming a parameter only the newer one declares resolves it. A module test has no label, - // which is what null says. + // which is what null says. 4.x has only the one overload, with displayNameIndex added, and + // the DependencyModules.xUnit4 build binds the same call to it. var details = TestIntrospectionHelper.GetTestCaseDetails( discoveryOptions, testMethod, diff --git a/src/DependencyModules.xUnit/Impl/XunitCurrentTestProvider.cs b/src/DependencyModules.xUnit/Impl/XunitCurrentTestProvider.cs new file mode 100644 index 0000000..a14eda0 --- /dev/null +++ b/src/DependencyModules.xUnit/Impl/XunitCurrentTestProvider.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using DependencyModules.Testing.Attributes.Interfaces; +using DependencyModules.Testing.Impl; +using Xunit; +using Xunit.v3; + +namespace DependencyModules.xUnit.Impl; + +/// +/// Reads the running test from xUnit's . +/// +/// +/// +/// Installed from the static constructors of and of +/// ModuleTestAttribute. xUnit creates both while it discovers or deserializes the tests, +/// before the first one runs. +/// +/// +/// xUnit has no running test while a case builds its containers: the case creates its tests, and +/// the container for each, before xUnit runs the first of them. So and +/// are null in the hooks that build a container, and a line written there +/// is dropped. answers there, from the test class. +/// +/// +internal sealed class XunitCurrentTestProvider : ICurrentTestProvider +{ + public object? Key => TestContext.Current.Test; + + public string? DisplayName => TestContext.Current.Test?.TestDisplayName; + + public Assembly? Assembly => + TestContext.Current.TestClass is IXunitTestClass testClass + ? testClass.Class.Assembly + : null; + + public bool TryWriteLine(string message) + { + if (TestContext.Current.TestOutputHelper is not { } output) + { + return false; + } + + try + { + output.WriteLine(message); + + return true; + } + // The flow still carries the context of a test that has finished, and xUnit's helper + // refuses output that it can no longer report. + catch (InvalidOperationException) + { + return false; + } + } + + public static void Install() => CurrentTest.Provider ??= new XunitCurrentTestProvider(); +} diff --git a/src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj b/src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj new file mode 100644 index 0000000..0a31985 --- /dev/null +++ b/src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj @@ -0,0 +1,54 @@ + + + + $(LibraryTargetFrameworks) + enable + enable + True + DependencyModules.xUnit4 + $(DefineConstants);XUNIT_V4 + xUnit v3 support for DependencyModules, for xunit.v3 version 4. [ModuleTest] builds a service provider from your modules for each test and each data row. The test parameters get their values from this service provider. For xunit.v3 version 3, use DependencyModules.xUnit. + true + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs b/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs index e28ad66..ea4bae4 100644 --- a/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs +++ b/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs @@ -27,11 +27,23 @@ public void RuntimeApi() Snapshot.Match(ApiOf(typeof(DependencyModuleAttribute))); } +#if XUNIT_V4 + /// + /// DependencyModules.xUnit4, which is the xUnit package's sources built against xunit.v3 4.x. + /// The two surfaces differ where the xUnit types they expose differ. + /// + [Fact] + public void XUnit4Api() + { + Snapshot.Match(ApiOf(typeof(ModuleTestAttribute))); + } +#else [Fact] public void XUnitApi() { Snapshot.Match(ApiOf(typeof(ModuleTestAttribute))); } +#endif /// /// The NUnit integration. Its [ModuleTest] shares a name with xUnit's and nothing else — diff --git a/tests/DependencyModules.Tests/DependencyModules.Tests.csproj b/tests/DependencyModules.Tests/DependencyModules.Tests.csproj index 282c4c7..314533c 100644 --- a/tests/DependencyModules.Tests/DependencyModules.Tests.csproj +++ b/tests/DependencyModules.Tests/DependencyModules.Tests.csproj @@ -7,12 +7,17 @@ False + + + $(DefineConstants);XUNIT_V4 + + - + +