Upgrade NUnit to v4; misc test improvements, #1001 - #1271
Conversation
|
Marking as draft until NUnit 4.6 is released with my merged PR (nunit/nunit#5236) included, to remove the reflection for ArgDisplayNames. However, the rest is still able to be reviewed. cc @NightOwl888 |
|
NUnit 4.6 is now out with my PR fix included, so I'll pick this up again today. |
NUnit 4.6 added Action overloads alongside the existing TestDelegate ones, making `() => action()` lambdas ambiguous. Pass the Action directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cyLevelTaskScheduler Add tests covering the custom NUnit-derived attributes (Nightly/Weekly/AwaitsFix/ Slow), the LuceneDelegatingTestCommand exception-recording messaging tap, RandomizedContext, and the LuceneRandomSeedInitializer seed-wiring used by the custom fixture builder. The attribute tests read the current tests:nightly/weekly/ awaitsfix/slow flags and assert the matching run/skip branch, so they pass regardless of how the suite is configured. Also add the missing `using Assert = Lucene.Net.TestFramework.Assert;` alias to TestLimitedConcurrencyLevelTaskScheduler.cs, which was binding Assert.Throws to NUnit's ambiguous TestDelegate/Action overloads (CS0121) after the global using revert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y test
Cast RandomGenerator to J2N.Randomizer before calling NextInt64(), since
System.Random.NextInt64() does not exist on .NET Framework (net48/net472),
which broke the TestFramework.NUnitExtensions build on those targets.
Also add GenerateRandomSeedsIsReproducibleFromTheInitialSeed, verifying the
fixture-builder seeding ("the real risk area") reproduces the exact per-test
seed sequence from a single initial seed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…estCase lifecycle Close the two remaining test-coverage gaps for the NUnit integration: - TestLuceneTestFrameworkInitializer (in Lucene.Net.Tests.TestFramework, which has InternalsVisibleTo to Lucene.Net): asserts InitializeStaticState wires the Lucene.ExceptionExtensions NUnit*Type mappings to the correct types, so a future NUnit rename/move that nulls a mapping is caught directly. - LuceneTestCaseLifecycleTests + LifecycleRecordingFixture: assert LuceneTestCase's OneTimeSetUp/SetUp/TearDown/OneTimeTearDown fire in the expected order (once-each bookends, each test wrapped by SetUp/TearDown), guarding the NUnit lifecycle integration directly rather than only via the repeating-tests fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guards that applying an NUnit pre-filter (as an IDE or category/name filter does during discovery) does not change the per-test seed a surviving test receives. NUnitTestFixtureBuilder advances the Randomizer for every method in deterministic order regardless of the filter, so a filtered-out method still consumes its slot and survivors keep their seeds. Covers the "no filters on the tests" requirement raised in the NUnit 4 upgrade review (apache#1001). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssembly
ExceptionScanningTestCase's static initializer probes for .NET Framework-only
exception types via Type.GetType("...mscorlib"). On modern .NET those types do
not exist, so the runtime raises AssemblyLoadContext.Resolving. NUnit 4's test
host (TestAssemblyResolver / AdditionalRuntimesStrategy) handles that event by
probing Microsoft.WindowsDesktop.App and calling LoadFromAssemblyPath() on a
reference (Version=0.0.0.0) copy of System.Security.Permissions.dll. Reference
assemblies cannot be loaded for execution, so it throws BadImageFormatException
inside the Type.GetType() call, which escaped the static constructor as a
TypeInitializationException and failed every test in the class.
NUnit 3's host did not hook the Resolving event this way, so this only began
failing after the NUnit 4 upgrade. Route the probes through a SafeGetType()
helper that returns null on BadImageFormatException/FileLoadException, restoring
the intended "type not found" result on modern .NET. No coverage is removed:
these probes only resolve a non-null type on .NET Framework, where the types
load from the real mscorlib and no resolver fallback occurs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Point the SafeGetType comment at the upstream bug report (nunit/nunit-console#1835) and note that this workaround can be removed once the resolver is fixed and we move to an NUnit3TestAdapter version that includes the fix. Replaces the earlier speculative issue references. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@NightOwl888 Thanks again for the incredibly thorough writeup and discussion above. I went back through the discussion carefully, and I want to acknowledge up front that your concerns were legitimate, and the core argument was right. The parts of our test framework that hook directly into NUnit are the parts most at risk from an upgrade, and we did not have adequate test coverage to protect that. So, I went and closed those test coverage gaps to protect this and future NUnit upgrades. If these tests ever fail, we'll know that the version of NUnit is not compatible with our current test framework. Below are your concerns, mapped to test coverage. The seeding hackThis was the real risk area that got the most attention. The hack is to seed every test reproducibly during discovery, and the requirements are: no filters when seeding, stable ordering, a single shared randomizer, and correct error routing. These are now covered by:
Exception mappingThis one is about our catch blocks depending on the NUnit exception types being mapped into
Lifecycle ordering
NUnit messaging / failure state
Custom attributes
On the other points discussed: I don't believe a full RandomizedTesting port is a prerequisite here. I agree that it's the right long-term direction, and nothing in this PR works against that later on. But coupling the NUnit 4 upgrade to that port turns it into a major effort that could cause this to miss our release window or delay the release further. So I took "Option 4" and added test coverage instead, to protect against any issues. I believe I demonstrated earlier in the discussion that I acknowledge and greatly appreciate the work taken to date (mostly by you) to get our tests working so well and being so stable. I hope my efforts here demonstrate that I take that seriously, and I've added long-desired test coverage to protect this work from future disruption. These changes will not only allow us to upgrade now, and ship an up-to-date dependency for TestFramework to our users, but also will enable us to stay up-to-date (even with Renovate automatically) safely. |
Upgrade NUnit to v4; misc test improvements.
Fixes #1001
Description
This PR upgrades NUnit to v4. For more details on some of the migration, see the docs here: https://docs.nunit.org/articles/nunit/release-notes/Nunit4.0-MigrationGuide.html
NUnit 4 dropped support for netstandard2.0; thus, this PR drops support for it from the TestFramework project. This only really would affect users who need to create an intermediate unit testing library (note: not a unit test project) that targets netstandard2.0 and references TestFramework. We expect this to not have a wide impact given current relatively low usage of TestFramework on NuGet (~100 downloads/mo). But if this is you, there is a simple workaround: either choose a single supported target (i.e. net8.0 or net462), or multi-target (i.e.
<TargetFrameworks>net462;net8.0</TargetFrameworks>in your project). Getting Lucene.NET on NUnit 4 will be more impactful in the long run and less restrictive to users than having to support a limited set of netstandard2.0 runtimes that are still in use (i.e. maybe Mono and some versions of Unity), and again this is just for unit tests.My first pass at this added a global using alias for Assert to point it to our Lucene.Net.TestFramework.Assert class, but that created way too many files changed in this PR, and it can be added later. So while there are some namespace cleanups here, I tried to minimize the amount of files changed since the other stuff is more important. Also, this upgrade exposed several places where we were not using our Assert class, so that has been fixed in several places.
One notable breaking change in NUnit 4.5 and later is that it now throws at runtime if you try to use the TimeoutAttribute on modern .NET. This was previously documented as not working on .NET 5 and later, but now it is an exception. To solve this at the class/test level, it was replaced with CancelAfterAttribute, which will trigger a CancellationToken parameter if the test runs longer than the configured value. So CancellationToken support was added to most of these tests and threaded through to calls where it made sense to do so. Only one test did not have an obvious seam since ForceMerge is not currently cancelable.
For TimeoutAttribute at the assembly level, there is not a good replacement. So I've removed it, since the existing
--blame-hang-timeoutlikely already covers this for both .NET Framework and modern .NET.Assert saw many documentation and correctness improvements in this PR; too many to mention. Notably, it switched to XML doc comments. Additionally, many unit tests were added for the NUnit-derived attributes.
The private/internal ArgDisplayNames property in NUnit changed to be an auto-prop from a declared field, so the reflection code that pokes into that has been updated to get the internal property instead of the private field. Unit tests were also added for this functionality to help detect and prevent regressions. Although if this regresses with a newer version of NUnit, the code should safely fall back to just using the ToString representation of the class fixture arguments, if anyone is using that (note that we are not using this functionality in our code).
Finally, a concurrency bug in TestRAMDirectory was fixed (which might have been surfaced by the NUnit 4 upgrade), which has the benefit of better matching the upstream Java code anyways. The root case was two different runs of the test could be writing to the folder at the same time.
AI: This was largely done by hand, but some repetitive parts (like removing usings) and some unit tests were done with the help of Claude Code.