This is an automated email from the ASF dual-hosted git repository.

nightowl888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucenenet.git


The following commit(s) were added to refs/heads/master by this push:
     new fa630daba Added RepeatAttribute that is aware of RandomizedContext 
(#1125)
fa630daba is described below

commit fa630dabaff833a5c4dddb72157ccddd32fcf08c
Author: Shad Storhaug <[email protected]>
AuthorDate: Mon Jan 27 21:35:57 2025 +0700

    Added RepeatAttribute that is aware of RandomizedContext (#1125)
    
    * Lucene.Net.Util.LuceneTestCase.LuceneDelegatingTestCommand::Execute(): 
Only create an instance of TestResult if it is not already instantiated.
    
    * Lucene.Net.Util.LuceneTestCase: Added RepeatAttribute that is 
RandomizedContext aware. Added tests from NUnit to verify it passes 
RepeatAttribute tests.
    
    * SWEEP: Added URLs of source code from randomizedtesting and NUnit to 
files that were copied from those libraries
    
    * Lucene.Net.TestData.RepeatingTests.RepeatingTestsFixtureBase: Added 
asserts to ensure the random seed and test seed in RandomizedContext are being 
updated consistently and correctly
    
    * Lucene.Net.Util.LuceneTestCase.RepeatAttribute: Added more robust 
subclass checking and a test to ensure that we receive the correct result
    
    * publish-test-results-for-test-projects.yml: Added 
Lucene.Net.Tests.TestFramework.NUnitExtensions
    
    * .github/workflows: Generated 
Lucene-Net-Tests-TestFramework-NUnitExtensions workflow file
    
    * LICENSE.txt: Added attribution of new test projects for NUnit extensions
    
    * Lucene.Net.Util.LuceneRandomSeedInitializer: Removed TODO
    
    * Lucene.Net.Util.LuceneTestCase.RepeatAttribute: Fixed method name of 
SetResultErrorNonLuceneTestCaseSubclass
---
 .../publish-test-results-for-test-projects.yml     |  10 +
 ...ene-Net-Tests-TestFramework-NUnitExtensions.yml | 123 ++++++++++
 LICENSE.txt                                        |   4 +-
 Lucene.Net.sln                                     |  13 ++
 .../Lucene.Net.TestFramework.csproj                |   2 +
 .../Support/Util/DefaultNamespaceTypeWrapper.cs    |   4 +-
 .../Support/Util/DisposableResourceInfo.cs         |   4 +-
 .../Support/Util/LifecycleScope.cs                 |   4 +-
 .../Support/Util/LuceneRandomSeedInitializer.cs    |  26 +--
 .../Support/Util/LuceneTestCase.RepeatAttribute.cs | 145 ++++++++++++
 .../Util/LuceneTestCase.TestFixtureAttribute.cs    |   4 +-
 .../Support/Util/NUnitTestFixtureBuilder.cs        |   4 +-
 .../Support/Util/RandomizedContext.cs              |  65 +++++-
 .../Support/Util/SeedUtils.cs                      |  13 +-
 .../Util/LuceneTestCase.cs                         |   9 +-
 .../RepeatAttributeTests.CustomMethodWrapper.cs    | 103 ++++++++
 .../RepeatAttributeTests.CustomTypeWrapper.cs      | 142 +++++++++++
 .../Attributes/RepeatAttributeTests.cs             | 163 +++++++++++++
 ....Net.Tests.TestFramework.NUnitExtensions.csproj |  52 +++++
 .../Lucene.Net.TestFramework.TestData.NUnit.csproj |  53 +++++
 .../RepeatingTests/RepeatedTestFixture.cs          | 195 ++++++++++++++++
 .../RepeatingTests/RepeatingTestsFixtureBase.cs    |  97 ++++++++
 .../TestUtilities/TestBuilder.cs                   | 260 +++++++++++++++++++++
 .../TypeExtensions.cs                              |  41 ++++
 24 files changed, 1501 insertions(+), 35 deletions(-)

diff --git a/.build/azure-templates/publish-test-results-for-test-projects.yml 
b/.build/azure-templates/publish-test-results-for-test-projects.yml
index 7e9d8988b..c6387ffe5 100644
--- a/.build/azure-templates/publish-test-results-for-test-projects.yml
+++ b/.build/azure-templates/publish-test-results-for-test-projects.yml
@@ -435,6 +435,16 @@ steps:
     testResultsArtifactName: '${{ parameters.testResultsArtifactName }}'
     testResultsFileName: '${{ parameters.testResultsFileName }}'
 
+- template: publish-test-results.yml
+  parameters:
+    testProjectName: 'Lucene.Net.Tests.TestFramework.NUnitExtensions'
+    osName: '${{ parameters.osName }}'
+    framework: '${{ parameters.framework }}'
+    vsTestPlatform: '${{ parameters.vsTestPlatform }}'
+    testResultsFormat: '${{ parameters.testResultsFormat }}'
+    testResultsArtifactName: '${{ parameters.testResultsArtifactName }}'
+    testResultsFileName: '${{ parameters.testResultsFileName }}'
+
 - template: publish-test-results.yml
   parameters:
     testProjectName: 'Lucene.Net.Tests.TestFramework'
diff --git 
a/.github/workflows/Lucene-Net-Tests-TestFramework-NUnitExtensions.yml 
b/.github/workflows/Lucene-Net-Tests-TestFramework-NUnitExtensions.yml
new file mode 100644
index 000000000..87dc90bf9
--- /dev/null
+++ b/.github/workflows/Lucene-Net-Tests-TestFramework-NUnitExtensions.yml
@@ -0,0 +1,123 @@
+####################################################################################
+# DO NOT EDIT: This file was automatically generated by 
Generate-TestWorkflows.ps1
+####################################################################################
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: 'Lucene.Net.Tests.TestFramework.NUnitExtensions'
+
+on:
+  workflow_dispatch:
+  pull_request:
+    paths:
+    - 'src/Lucene.Net.Tests.TestFramework.NUnitExtensions/**/*'
+    - '.build/dependencies.props'
+    - '.build/TestReferences.Common.*'
+    - 'TestTargetFrameworks.*'
+    - '.github/**/*.yml'
+    - '*.sln'
+    - 'src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Directory.Build.*'
+    - 'src/Directory.Build.*'
+    - 'Directory.Build.*'
+
+    # Dependencies
+    - 'src/Lucene.Net/**/*'
+    - 'src/Lucene.Net.Analysis.Common/**/*'
+    - 'src/Lucene.Net.Codecs/**/*'
+    - 'src/Lucene.Net.TestFramework/**/*'
+    - 'src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/**/*'
+
+    - '!**/*.md'
+
+jobs:
+
+  Test:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [windows-latest, ubuntu-latest]
+        framework: [net9.0, net8.0, net6.0, net48, net472]
+        platform: [x64]
+        configuration: [Release]
+        exclude:
+          - os: ubuntu-latest
+            framework: net48
+          - os: ubuntu-latest
+            framework: net472
+          - os: macos-latest
+            framework: net48
+          - os: macos-latest
+            framework: net472
+    env:
+      project_path: 
'./src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Lucene.Net.Tests.TestFramework.NUnitExtensions.csproj'
+      run_slow_tests: 'false'
+      trx_file_name: 'TestResults.trx'
+      md_file_name: 'TestResults.md' # Report file name for 
LiquidTestReports.Markdown
+
+    steps:
+      - name: Checkout Source Code
+        uses: actions/checkout@v3
+
+      - name: Disable .NET SDK Telemetry and Logo
+        run: |
+          echo "DOTNET_NOLOGO=1" | Out-File -FilePath  $env:GITHUB_ENV 
-Encoding utf8 -Append
+          echo "DOTNET_CLI_TELEMETRY_OPTOUT=1" | Out-File -FilePath  
$env:GITHUB_ENV -Encoding utf8 -Append
+        shell: pwsh
+
+      - name: Setup .NET 6 SDK
+        uses: actions/setup-dotnet@v3
+        with:
+          dotnet-version: '6.0.x'
+        if: ${{ startswith(matrix.framework, 'net6.') }}
+
+      - name: Setup .NET 8 SDK
+        uses: actions/setup-dotnet@v3
+        with:
+          dotnet-version: '8.0.x'
+
+      - name: Setup .NET 9 SDK
+        uses: actions/setup-dotnet@v3
+        with:
+          dotnet-version: '9.0.x'
+
+      - name: Setup Environment Variables
+        run: |
+          $project_name = 
[System.IO.Path]::GetFileNameWithoutExtension($env:project_path)
+          $test_results_artifact_name = 
"testresults_${{matrix.os}}_${{matrix.framework}}_${{matrix.platform}}_${{matrix.configuration}}"
+          $working_directory = "$env:GITHUB_WORKSPACE"
+          Write-Host "Project Name: $project_name"
+          Write-Host "Results Artifact Name: $test_results_artifact_name"
+          Write-Host "Working Directory: $working_directory"
+          echo "project_name=$project_name" | Out-File -FilePath  
$env:GITHUB_ENV -Encoding utf8 -Append
+          echo "test_results_artifact_name=$test_results_artifact_name" | 
Out-File -FilePath  $env:GITHUB_ENV -Encoding utf8 -Append
+          # Set the Azure DevOps default working directory env variable, so 
our tests only need to deal with a single env variable
+          echo "SYSTEM_DEFAULTWORKINGDIRECTORY=$working_directory" | Out-File 
-FilePath  $env:GITHUB_ENV -Encoding utf8 -Append
+          # Title for LiquidTestReports.Markdown
+          echo "title=Test Run for $project_name - ${{matrix.framework}} - 
${{matrix.platform}} - ${{matrix.os}}" | Out-File -FilePath  $env:GITHUB_ENV 
-Encoding utf8 -Append
+        shell: pwsh
+      - run: dotnet build ${{env.project_path}} --configuration 
${{matrix.configuration}} --framework ${{matrix.framework}} 
/p:TestFrameworks=true
+      - run: dotnet test ${{env.project_path}} --configuration 
${{matrix.configuration}} --framework ${{matrix.framework}} --no-build 
--no-restore --blame-hang --blame-hang-dump-type mini --blame-hang-timeout 
20minutes --logger:"console;verbosity=normal" 
--logger:"trx;LogFileName=${{env.trx_file_name}}" 
--logger:"liquid.md;LogFileName=${{env.md_file_name}};Title=${{env.title}};" 
--results-directory:"${{github.workspace}}/${{env.test_results_artifact_name}}/${{env.project_name}}"
 -- RunCo [...]
+        shell: bash
+      # upload reports as build artifacts
+      - name: Upload a Build Artifact
+        uses: actions/upload-artifact@v4
+        if: ${{always()}}
+        with:
+          name: '${{env.test_results_artifact_name}}'
+          path: '${{github.workspace}}/${{env.test_results_artifact_name}}'
+
diff --git a/LICENSE.txt b/LICENSE.txt
index fe107390a..fc4956c57 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -735,6 +735,8 @@ Some code in
 src/Lucene.Net.TestFramework/Support/Util/DefaultNamespaceTypeWrapper.cs
 
src/Lucene.Net.TestFramework/Support/Util/LuceneTestCase.TestFixtureAttribute.cs
 src/Lucene.Net.TestFramework/Support/Util/NUnitTestFixtureBuilder.cs
+src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/*
+src/Lucene.Net.Tests.TestFramework.NUnitExtensions/*
 falls under the following license:
 
 // Copyright (c) 2021 Charlie Poole, Rob Prouse
@@ -798,4 +800,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (
 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
PROFITS; OR BUSINESS
 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR
 TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
THIS SOFTWARE, EVEN IF
-ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Lucene.Net.sln b/Lucene.Net.sln
index 2002610c9..e5ec54e0a 100644
--- a/Lucene.Net.sln
+++ b/Lucene.Net.sln
@@ -202,6 +202,10 @@ Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = 
"build", "proj\build.msbuild
 EndProject
 Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "github", 
"proj\github.msbuildproj", "{E71152A0-48CC-4334-981F-F5FBFFA50891}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = 
"Lucene.Net.TestFramework.TestData.NUnit", 
"src\dotnet\Lucene.Net.TestFramework.TestData.NUnit\Lucene.Net.TestFramework.TestData.NUnit.csproj",
 "{6E131ACF-D27A-45AF-923B-A1932EB11E2D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = 
"Lucene.Net.Tests.TestFramework.NUnitExtensions", 
"src\Lucene.Net.Tests.TestFramework.NUnitExtensions\Lucene.Net.Tests.TestFramework.NUnitExtensions.csproj",
 "{724CFF92-5ED6-4F7D-9D2C-53ABA0589F37}"
+EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Debug|Any CPU = Debug|Any CPU
@@ -488,6 +492,14 @@ Global
                {E71152A0-48CC-4334-981F-F5FBFFA50891}.Debug|Any CPU.Build.0 = 
Debug|Any CPU
                {E71152A0-48CC-4334-981F-F5FBFFA50891}.Release|Any 
CPU.ActiveCfg = Release|Any CPU
                {E71152A0-48CC-4334-981F-F5FBFFA50891}.Release|Any CPU.Build.0 
= Release|Any CPU
+               {6E131ACF-D27A-45AF-923B-A1932EB11E2D}.Debug|Any CPU.ActiveCfg 
= Debug|Any CPU
+               {6E131ACF-D27A-45AF-923B-A1932EB11E2D}.Debug|Any CPU.Build.0 = 
Debug|Any CPU
+               {6E131ACF-D27A-45AF-923B-A1932EB11E2D}.Release|Any 
CPU.ActiveCfg = Release|Any CPU
+               {6E131ACF-D27A-45AF-923B-A1932EB11E2D}.Release|Any CPU.Build.0 
= Release|Any CPU
+               {724CFF92-5ED6-4F7D-9D2C-53ABA0589F37}.Debug|Any CPU.ActiveCfg 
= Debug|Any CPU
+               {724CFF92-5ED6-4F7D-9D2C-53ABA0589F37}.Debug|Any CPU.Build.0 = 
Debug|Any CPU
+               {724CFF92-5ED6-4F7D-9D2C-53ABA0589F37}.Release|Any 
CPU.ActiveCfg = Release|Any CPU
+               {724CFF92-5ED6-4F7D-9D2C-53ABA0589F37}.Release|Any CPU.Build.0 
= Release|Any CPU
        EndGlobalSection
        GlobalSection(SolutionProperties) = preSolution
                HideSolutionNode = FALSE
@@ -506,6 +518,7 @@ Global
                {4D0ED7D9-ABEE-4890-B06C-477E3A32B9A0} = 
{E5E8C5DC-7048-4818-B884-FB2D037D2EF2}
                {FED4A824-1F32-4948-8D37-2B7610804DB5} = 
{42599646-275F-4970-BC60-A3349F6498CC}
                {C0448DD3-68D2-485F-B31A-D2806E589FA7} = 
{42599646-275F-4970-BC60-A3349F6498CC}
+               {6E131ACF-D27A-45AF-923B-A1932EB11E2D} = 
{8CA61D33-3590-4024-A304-7B1F75B50653}
        EndGlobalSection
        GlobalSection(ExtensibilityGlobals) = postSolution
                SolutionGuid = {9F2179CC-CFD2-4419-AB74-D72856931F36}
diff --git a/src/Lucene.Net.TestFramework/Lucene.Net.TestFramework.csproj 
b/src/Lucene.Net.TestFramework/Lucene.Net.TestFramework.csproj
index d10a277d3..406b4075d 100644
--- a/src/Lucene.Net.TestFramework/Lucene.Net.TestFramework.csproj
+++ b/src/Lucene.Net.TestFramework/Lucene.Net.TestFramework.csproj
@@ -114,6 +114,8 @@
     <InternalsVisibleTo Include="Lucene.Net.Tests.Suggest" />
     <InternalsVisibleTo Include="Lucene.Net.Tests.TestFramework" />
     <InternalsVisibleTo 
Include="Lucene.Net.Tests.TestFramework.DependencyInjection" />
+
+    <InternalsVisibleTo Include="Lucene.Net.TestFramework.TestData.NUnit" />
   </ItemGroup>
 
 </Project>
diff --git 
a/src/Lucene.Net.TestFramework/Support/Util/DefaultNamespaceTypeWrapper.cs 
b/src/Lucene.Net.TestFramework/Support/Util/DefaultNamespaceTypeWrapper.cs
index 4a3de06d1..571fcf40a 100644
--- a/src/Lucene.Net.TestFramework/Support/Util/DefaultNamespaceTypeWrapper.cs
+++ b/src/Lucene.Net.TestFramework/Support/Util/DefaultNamespaceTypeWrapper.cs
@@ -1,4 +1,6 @@
-using NUnit.Framework.Interfaces;
+// Based on: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/framework/Internal/TypeWrapper.cs
+
+using NUnit.Framework.Interfaces;
 using NUnit.Framework.Internal;
 using System;
 using System.Linq;
diff --git 
a/src/Lucene.Net.TestFramework/Support/Util/DisposableResourceInfo.cs 
b/src/Lucene.Net.TestFramework/Support/Util/DisposableResourceInfo.cs
index d14fce353..df1bb56d5 100644
--- a/src/Lucene.Net.TestFramework/Support/Util/DisposableResourceInfo.cs
+++ b/src/Lucene.Net.TestFramework/Support/Util/DisposableResourceInfo.cs
@@ -1,4 +1,6 @@
-using System;
+// Source: 
https://github.com/randomizedtesting/randomizedtesting/blob/release/2.7.8/randomized-runner/src/main/java/com/carrotsearch/randomizedtesting/CloseableResourceInfo.java
+
+using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
diff --git a/src/Lucene.Net.TestFramework/Support/Util/LifecycleScope.cs 
b/src/Lucene.Net.TestFramework/Support/Util/LifecycleScope.cs
index 92e592ac5..dd51fc7b4 100644
--- a/src/Lucene.Net.TestFramework/Support/Util/LifecycleScope.cs
+++ b/src/Lucene.Net.TestFramework/Support/Util/LifecycleScope.cs
@@ -1,4 +1,6 @@
-using System;
+// Source: 
https://github.com/randomizedtesting/randomizedtesting/blob/release/2.7.8/randomized-runner/src/main/java/com/carrotsearch/randomizedtesting/LifecycleScope.java
+
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
diff --git 
a/src/Lucene.Net.TestFramework/Support/Util/LuceneRandomSeedInitializer.cs 
b/src/Lucene.Net.TestFramework/Support/Util/LuceneRandomSeedInitializer.cs
index 5d95ef7a7..d7cbed319 100644
--- a/src/Lucene.Net.TestFramework/Support/Util/LuceneRandomSeedInitializer.cs
+++ b/src/Lucene.Net.TestFramework/Support/Util/LuceneRandomSeedInitializer.cs
@@ -29,7 +29,7 @@ internal class LuceneRandomSeedInitializer
         #region Messages
 
         const string RANDOM_SEED_PARAMS_MSG =
-            "\"tests:seed\" parameter must be a valid long hexadecimal value 
or the word \"random\".";
+            "\"tests:seed\" parameter must be one or two valid long 
hexadecimal values separated by a \":\" or the word \"random\".";
 
         #endregion
 
@@ -47,7 +47,6 @@ internal class LuceneRandomSeedInitializer
         /// <returns><c>true</c> if the seed was found in context; 
<c>false</c> if the seed was generated.</returns>
         private static bool TryGetRandomSeedsFromContext(Test test, out long 
seed, out long? testSeed)
         {
-            //bool generate;
             seed = default;
             testSeed = default;
             string seedAsString;
@@ -58,7 +57,6 @@ private static bool TryGetRandomSeedsFromContext(Test test, 
out long seed, out l
             if (randomSeedAttribute != null)
             {
                 seedAsString = randomSeedAttribute.RandomSeed;
-                //generate = false;
             }
             else
             {
@@ -68,31 +66,25 @@ private static bool TryGetRandomSeedsFromContext(Test test, 
out long seed, out l
 
             if (seedAsString is null || "random".Equals(seedAsString, 
StringComparison.OrdinalIgnoreCase))
             {
-                //generate = true;
                 return false;
             }
 
             int colonIndex = seedAsString.IndexOf(':');
             if (colonIndex  != -1)
             {
-                if (!J2N.Numerics.Int64.TryParse(seedAsString, 0, colonIndex, 
radix: 16, out seed))
+                if (!J2N.Numerics.Int64.TryParse(seedAsString.AsSpan(0, 
colonIndex), radix: 16, out seed))
                 {
                     test.MakeInvalid(RANDOM_SEED_PARAMS_MSG);
                     return false;
                 }
 
-                // LUCENENET TODO: For now, we are ignoring anything after a 
colon in the string, but logically it seems like
-                // a second seed would be to set the a test so the 
RandomAttribute fails on the first iteration. Lucene uses a compound
-                // seed, but it isn't clear from analyzing the source how it 
is used, just that it can contain any number of colon delimited
-                // values. If we ignore now, we leave the door open for adding 
a compound seed in the most sensible way later without breaking
-                // the current version when the change is introduced.
-                //if (!J2N.Numerics.Int64.TryParse(seedAsString, colonIndex + 
1, seedAsString.Length - (colonIndex + 1), radix: 16, out long testSeedValue))
-                //{
-                //    test.MakeInvalid(RANDOM_SEED_PARAMS_MSG);
-                //    return false;
-                //}
-
-                //testSeed = testSeedValue;
+                if 
(!J2N.Numerics.Int64.TryParse(seedAsString.AsSpan(colonIndex + 1, 
seedAsString.Length - (colonIndex + 1)), radix: 16, out long testSeedValue))
+                {
+                    test.MakeInvalid(RANDOM_SEED_PARAMS_MSG);
+                    return false;
+                }
+
+                testSeed = testSeedValue;
                 return true;
             }
             else if (J2N.Numerics.Int64.TryParse(seedAsString, radix: 16, out 
seed))
diff --git 
a/src/Lucene.Net.TestFramework/Support/Util/LuceneTestCase.RepeatAttribute.cs 
b/src/Lucene.Net.TestFramework/Support/Util/LuceneTestCase.RepeatAttribute.cs
new file mode 100644
index 000000000..5a0b4beb5
--- /dev/null
+++ 
b/src/Lucene.Net.TestFramework/Support/Util/LuceneTestCase.RepeatAttribute.cs
@@ -0,0 +1,145 @@
+using NUnit.Framework.Interfaces;
+using NUnit.Framework.Internal;
+using NUnit.Framework.Internal.Commands;
+using System;
+#nullable enable
+
+namespace Lucene.Net.Util
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF licenses this file to You under the Apache License, Version 2.0
+     * (the "License"); you may not use this file except in compliance with
+     * the License.  You may obtain a copy of the License at
+     *
+     *     http://www.apache.org/licenses/LICENSE-2.0
+     *
+     * Unless required by applicable law or agreed to in writing, software
+     * distributed under the License is distributed on an "AS IS" BASIS,
+     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     * See the License for the specific language governing permissions and
+     * limitations under the License.
+     */
+
+    public abstract partial class LuceneTestCase
+    {
+        /// <summary>
+        /// Specifies that a test should be run multiple times. If any 
repetition fails,
+        /// the remaining ones are not run and a failure is reported.
+        /// </summary>
+        /// <remarks>
+        /// This attribute differs from <see 
cref="NUnit.Framework.RepeatAttribute"/> in that
+        /// it is aware of <see cref="RandomizedContext"/> and will reset the 
test seed on each
+        /// iteration. As a result, if there is a test failure, the seed that 
is reported will
+        /// duplicate the exact test conditions on the first try.
+        /// </remarks>
+        [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, 
Inherited = false)]
+        public class RepeatAttribute : System.Attribute, IRepeatTest
+        {
+            private readonly int repeatCount;
+
+            /// <summary>
+            /// Initializes a new instance of <see cref="RepeatAttribute"/>.
+            /// </summary>
+            /// <param name="repeatCount">The number of times to run the 
test.</param>
+            public RepeatAttribute(int repeatCount)
+            {
+                this.repeatCount = repeatCount;
+            }
+
+            /// <summary>
+            /// Wrap a command and return the result.
+            /// </summary>
+            /// <param name="command">The command to be wrapped.</param>
+            /// <returns>The wrapped command.</returns>
+            public TestCommand Wrap(TestCommand command)
+            {
+                return new RepeatTestCommand(command, repeatCount);
+            }
+
+            /// <summary>
+            /// The test command for the <see cref="RepeatAttribute"/>.
+            /// </summary>
+            private class RepeatTestCommand : BeforeAndAfterTestCommand
+            {
+                private readonly int repeatCount;
+
+                /// <summary>
+                /// Initializes a new instance of the <see 
cref="RepeatTestCommand"/> class.
+                /// </summary>
+                /// <param name="innerCommand">The inner command.</param>
+                /// <param name="repeatCount">The number of repetitions</param>
+                public RepeatTestCommand(TestCommand innerCommand, int 
repeatCount)
+                    : base(innerCommand)
+                {
+                    this.repeatCount = repeatCount;
+                }
+
+                /// <summary>
+                /// Runs the test, saving a <see cref="TestResult"/> in the 
supplied <see cref="TestExecutionContext"/>.
+                /// </summary>
+                /// <param name="context">The context in which the test should 
run.</param>
+                /// <returns>A <see cref="TestResult"/>.</returns>
+                public override TestResult Execute(TestExecutionContext 
context)
+                {
+                    if (context.CurrentTest.TypeInfo is null || 
!context.CurrentTest.TypeInfo.Type.IsSubclassOf(typeof(LuceneTestCase)))
+                    {
+                        return 
SetResultErrorNonLuceneTestCaseSubclass(context);
+                    }
+
+                    RandomizedContext? randomizedContext = 
context.CurrentTest.GetRandomizedContext();
+                    if (randomizedContext is null)
+                    {
+                        return 
SetResultErrorNonLuceneTestCaseSubclass(context);
+                    }
+
+                    var random = new 
J2N.Randomizer(randomizedContext.RandomSeed);
+
+                    for (int i = 0; i < repeatCount; i++)
+                    {
+                        // Regenerate the test seed for this iteration
+                        randomizedContext.ResetSeed(testSeed: 
random.NextInt64());
+
+                        try
+                        {
+                            // Execute the SetUp, Test, and TearDown with the 
seed
+                            context.CurrentResult = 
innerCommand.Execute(context);
+                        }
+                        catch (Exception ex)
+                        {
+                            if (context.CurrentResult is null) 
context.CurrentResult = context.CurrentTest.MakeTestResult();
+                            context.CurrentResult.RecordException(ex);
+                        }
+
+                        if (context.CurrentResult.ResultState != 
ResultState.Success)
+                        {
+                            if (context.CurrentResult.ResultState == 
ResultState.Failure || context.CurrentResult.ResultState == ResultState.Error)
+                            {
+                                string message = $"Repeat failed on iteration 
'{i}'.{Environment.NewLine}{Environment.NewLine}{context.CurrentResult.Message}";
+                                
context.CurrentResult.SetResult(context.CurrentResult.ResultState, message, 
context.CurrentResult.StackTrace);
+                            }
+
+                            break; // Only repeat for successuful test runs
+                        }
+
+                        context.CurrentRepeatCount++;
+                    }
+
+                    return context.CurrentResult;
+                }
+
+                private static TestResult 
SetResultErrorNonLuceneTestCaseSubclass(TestExecutionContext context)
+                {
+                    if (context.CurrentResult is null) context.CurrentResult = 
context.CurrentTest.MakeTestResult();
+                    // We only want this attribute to be used on subclasses of 
LuceneTestCase. This is an error.
+                    context.CurrentResult.SetResult(ResultState.Error,
+                        $"{typeof(RepeatAttribute).FullName} may only be used 
on a test in a subclass of {nameof(LuceneTestCase)}.");
+
+                    return context.CurrentResult;
+                }
+            }
+        }
+    }
+}
diff --git 
a/src/Lucene.Net.TestFramework/Support/Util/LuceneTestCase.TestFixtureAttribute.cs
 
b/src/Lucene.Net.TestFramework/Support/Util/LuceneTestCase.TestFixtureAttribute.cs
index 9e11835b6..55495808b 100644
--- 
a/src/Lucene.Net.TestFramework/Support/Util/LuceneTestCase.TestFixtureAttribute.cs
+++ 
b/src/Lucene.Net.TestFramework/Support/Util/LuceneTestCase.TestFixtureAttribute.cs
@@ -1,4 +1,6 @@
-using Lucene.Net.Support;
+// Based on: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/framework/Attributes/TestFixtureAttribute.cs
+
+using Lucene.Net.Support;
 using NUnit.Framework;
 using NUnit.Framework.Interfaces;
 using NUnit.Framework.Internal;
diff --git 
a/src/Lucene.Net.TestFramework/Support/Util/NUnitTestFixtureBuilder.cs 
b/src/Lucene.Net.TestFramework/Support/Util/NUnitTestFixtureBuilder.cs
index 5fc529b30..1eb7132be 100644
--- a/src/Lucene.Net.TestFramework/Support/Util/NUnitTestFixtureBuilder.cs
+++ b/src/Lucene.Net.TestFramework/Support/Util/NUnitTestFixtureBuilder.cs
@@ -1,4 +1,6 @@
-using NUnit.Framework;
+// Based on: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/framework/Internal/Builders/NUnitTestFixtureBuilder.cs
+
+using NUnit.Framework;
 using NUnit.Framework.Interfaces;
 using NUnit.Framework.Internal;
 using NUnit.Framework.Internal.Builders;
diff --git a/src/Lucene.Net.TestFramework/Support/Util/RandomizedContext.cs 
b/src/Lucene.Net.TestFramework/Support/Util/RandomizedContext.cs
index e198e1862..1ea36ec81 100644
--- a/src/Lucene.Net.TestFramework/Support/Util/RandomizedContext.cs
+++ b/src/Lucene.Net.TestFramework/Support/Util/RandomizedContext.cs
@@ -1,4 +1,6 @@
-using Lucene.Net.Support.Threading;
+// Rougly similar to: 
https://github.com/randomizedtesting/randomizedtesting/blob/release/2.7.8/randomized-runner/src/main/java/com/carrotsearch/randomizedtesting/RandomizedContext.java
+
+using Lucene.Net.Support.Threading;
 using NUnit.Framework.Interfaces;
 using NUnit.Framework.Internal;
 using System;
@@ -42,12 +44,12 @@ internal class RandomizedContext
         internal const string RandomizedContextThreadNameKeyName = 
"_RandomizedContext_ThreadName";
         internal const string RandomizedContextStackTraceKeyName = 
"_RandomizedContext_StackTrace";
 
-        private readonly ThreadLocal<Random> randomGenerator;
+        private readonly ThreadLocal<J2N.Randomizer> randomGenerator;
         private readonly Test currentTest;
         private readonly Assembly currentTestAssembly;
         private readonly long randomSeed;
         private volatile string? randomSeedAsString;
-        private readonly long testSeed;
+        private long testSeed;
 
         /// <summary>
         /// Disposable resources.
@@ -72,7 +74,7 @@ public RandomizedContext(Test currentTest, Assembly 
currentTestAssembly, long ra
             this.currentTestAssembly = currentTestAssembly ?? throw new 
ArgumentNullException(nameof(currentTestAssembly));
             this.randomSeed = randomSeed;
             this.testSeed = testSeed;
-            this.randomGenerator = new ThreadLocal<Random>(() => new 
J2N.Randomizer(this.testSeed));
+            this.randomGenerator = new ThreadLocal<J2N.Randomizer>(() => new 
J2N.Randomizer(this.testSeed));
         }
 
         /// <summary>
@@ -83,7 +85,7 @@ public RandomizedContext(Test currentTest, Assembly 
currentTestAssembly, long ra
         /// <summary>
         /// Gets the initial seed as a hexadecimal string for 
display/configuration purposes.
         /// </summary>
-        public string RandomSeedAsString => randomSeedAsString ??= 
SeedUtils.FormatSeed(randomSeed);
+        public string RandomSeedAsString => randomSeedAsString ??= 
SeedUtils.FormatSeed(randomSeed, testSeed);
 
         /// <summary>
         /// The current test for this context.
@@ -98,7 +100,7 @@ public RandomizedContext(Test currentTest, Assembly 
currentTestAssembly, long ra
         /// <summary>
         /// The random seed for this test's <see cref="RandomGenerator"/>.
         /// </summary>
-        public long TestSeed => testSeed;
+        public long TestSeed => Interlocked.Read(ref this.testSeed);
 
         /// <summary>
         /// Gets the RandomGenerator specific to this Test and thread. This 
random generator implementatation
@@ -112,7 +114,25 @@ public RandomizedContext(Test currentTest, Assembly 
currentTestAssembly, long ra
         /// random test data in these cases. Using the <see 
cref="LuceneTestCase.TestFixtureAttribute"/>
         /// will set the seed properly and make it possible to repeat the 
result.
         /// </summary>
-        public Random RandomGenerator => randomGenerator.Value!;
+        public Random RandomGenerator
+        {
+            get
+            {
+                var random = randomGenerator.Value!;
+                UninterruptableMonitor.Enter(random.SyncRoot);
+                try
+                {
+                    // Ensure the current thread is using the latest test seed.
+                    if (random.Seed != testSeed)
+                        random.Seed = testSeed;
+                }
+                finally
+                {
+                    UninterruptableMonitor.Exit(random.SyncRoot);
+                }
+                return random;
+            }
+        }
 
         /// <summary>
         /// Gets the randomized context for the current test or test fixture.
@@ -304,5 +324,36 @@ private static void FormatException(Exception exception, 
StringBuilder destinati
                 destination.AppendLine(stackTrace.ToString());
             }
         }
+
+        /// <summary>
+        /// Resets the local <see cref="testSeed"/> field so it can be
+        /// used to update each thread that uses <see cref="randomGenerator"/> 
if
+        /// the seed doesn't match.
+        /// <para/>
+        /// Although calling this method is threadsafe, it should not be
+        /// called in the middle of a test. Instead, it should be called
+        /// between test runs to prevent non-repeatable random conditions
+        /// from occurring during a test.
+        /// </summary>
+        /// <param name="testSeed">The new test seed. This value will be
+        /// used to initialize the random generator for the test run.</param>
+        internal void ResetSeed(long testSeed)
+        {
+            var random = this.randomGenerator.Value!;
+            UninterruptableMonitor.Enter(random.SyncRoot);
+            try
+            {
+                this.randomSeedAsString = null;
+                this.testSeed = testSeed;
+                // Note that this resets the current thread only.
+                // That is why we have to check in the RandomGenerator
+                // property getter that the Seed is up-to-date.
+                random.Seed = testSeed;
+            }
+            finally
+            {
+                UninterruptableMonitor.Exit(random.SyncRoot);
+            }
+        }
     }
 }
diff --git a/src/Lucene.Net.TestFramework/Support/Util/SeedUtils.cs 
b/src/Lucene.Net.TestFramework/Support/Util/SeedUtils.cs
index c4a051339..b984fd309 100644
--- a/src/Lucene.Net.TestFramework/Support/Util/SeedUtils.cs
+++ b/src/Lucene.Net.TestFramework/Support/Util/SeedUtils.cs
@@ -1,4 +1,6 @@
-using J2N;
+// Based on: 
https://github.com/randomizedtesting/randomizedtesting/blob/release/2.7.8/randomized-runner/src/main/java/com/carrotsearch/randomizedtesting/SeedUtils.java
+
+using J2N;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -36,6 +38,13 @@ internal static class SeedUtils
         /// </summary>
         // LUCENENET: Our format deviates from the Java randomizedtesting 
implementation
         public static string FormatSeed(long seed)
-            => string.Concat("0x", seed.ToHexString());
+            => $"0x{seed:x16}";
+
+        /// <summary>
+        /// Format a <paramref name="seed"/> and <paramref name="testSeed"/>.
+        /// </summary>
+        // LUCENENET: Our format deviates from the Java randomizedtesting 
implementation
+        public static string FormatSeed(long seed, long testSeed)
+            => $"0x{seed:x16}:0x{testSeed:x16}";
     }
 }
diff --git a/src/Lucene.Net.TestFramework/Util/LuceneTestCase.cs 
b/src/Lucene.Net.TestFramework/Util/LuceneTestCase.cs
index 65e711852..baf5c2887 100644
--- a/src/Lucene.Net.TestFramework/Util/LuceneTestCase.cs
+++ b/src/Lucene.Net.TestFramework/Util/LuceneTestCase.cs
@@ -208,8 +208,6 @@ public override TestResult Execute(TestExecutionContext 
context)
                     test.Properties.Set(PropertyNames.SkipReason, SkipReason);
                 }
 
-                context.CurrentResult = test.MakeTestResult();
-
                 if (!skip)
                 {
                     try
@@ -218,9 +216,14 @@ public override TestResult Execute(TestExecutionContext 
context)
                     }
                     catch (Exception ex)
                     {
+                        if (context.CurrentResult is null) 
context.CurrentResult = context.CurrentTest.MakeTestResult();
                         context.CurrentResult.RecordException(ex);
                     }
                 }
+                else if (context.CurrentResult is null)
+                {
+                    context.CurrentResult = 
context.CurrentTest.MakeTestResult();
+                }
 
                 return context.CurrentResult;
             }
@@ -972,7 +975,7 @@ System Properties
             // LUCENENET: DisposeAfterTest runs last
             try
             {
-                RandomizedContext.CurrentContext.DisposeResources();
+                RandomizedContext.CurrentContext?.DisposeResources();
             }
             catch (Exception ex) when (ex.IsThrowable())
             {
diff --git 
a/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.CustomMethodWrapper.cs
 
b/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.CustomMethodWrapper.cs
new file mode 100644
index 000000000..b425a72b3
--- /dev/null
+++ 
b/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.CustomMethodWrapper.cs
@@ -0,0 +1,103 @@
+// Source: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/tests/Attributes/RepeatAttributeTests.CustomMethodWrapper.cs
+
+using NUnit.Framework.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.Attributes
+{
+    #region Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT 
License.
+
+    // Copyright (c) 2021 Charlie Poole, Rob Prouse
+    // 
+    // Permission is hereby granted, free of charge, to any person obtaining a 
copy
+    // of this software and associated documentation files (the "Software"), 
to deal
+    // in the Software without restriction, including without limitation the 
rights
+    // to use, copy, modify, merge, publish, distribute, sublicense, and/or 
sell
+    // copies of the Software, and to permit persons to whom the Software is
+    // furnished to do so, subject to the following conditions:
+    // 
+    // The above copyright notice and this permission notice shall be included 
in
+    // all copies or substantial portions of the Software.
+    // 
+    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
OR
+    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
THE
+    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
IN
+    // THE SOFTWARE.
+
+    #endregion
+
+    public partial class RepeatAttributeTests
+    {
+        private sealed class CustomMethodWrapper : IMethodInfo
+        {
+            private readonly IMethodInfo _baseInfo;
+            private readonly Attribute[] _extraAttributes;
+
+            public CustomMethodWrapper(IMethodInfo baseInfo, Attribute[] 
extraAttributes)
+            {
+                _baseInfo = baseInfo;
+                _extraAttributes = extraAttributes;
+            }
+
+            public T[] GetCustomAttributes<T>(bool inherit) where T : class
+            {
+                return _baseInfo.GetCustomAttributes<T>(inherit)
+                    .Concat(_extraAttributes.OfType<T>())
+                    .ToArray();
+            }
+
+            public ITypeInfo TypeInfo => _baseInfo.TypeInfo;
+
+            public MethodInfo MethodInfo => _baseInfo.MethodInfo;
+
+            public string Name => _baseInfo.Name;
+
+            public bool IsAbstract => _baseInfo.IsAbstract;
+
+            public bool IsPublic => _baseInfo.IsPublic;
+
+            public bool IsStatic => _baseInfo.IsStatic;
+
+            public bool ContainsGenericParameters => 
_baseInfo.ContainsGenericParameters;
+
+            public bool IsGenericMethod => _baseInfo.IsGenericMethod;
+
+            public bool IsGenericMethodDefinition => 
_baseInfo.IsGenericMethodDefinition;
+
+            public ITypeInfo ReturnType => _baseInfo.ReturnType;
+
+            public Type[] GetGenericArguments()
+            {
+                return _baseInfo.GetGenericArguments();
+            }
+
+            public IParameterInfo[] GetParameters()
+            {
+                return _baseInfo.GetParameters();
+            }
+
+            public object Invoke(object fixture, params object[] args)
+            {
+                return _baseInfo.Invoke(fixture, args);
+            }
+
+            public bool IsDefined<T>(bool inherit) where T : class
+            {
+                return _baseInfo.IsDefined<T>(inherit);
+            }
+
+            public IMethodInfo MakeGenericMethod(params Type[] typeArguments)
+            {
+                return _baseInfo.MakeGenericMethod(typeArguments);
+            }
+        }
+    }
+}
diff --git 
a/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.CustomTypeWrapper.cs
 
b/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.CustomTypeWrapper.cs
new file mode 100644
index 000000000..a9a9285a1
--- /dev/null
+++ 
b/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.CustomTypeWrapper.cs
@@ -0,0 +1,142 @@
+// Source: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/tests/Attributes/RepeatAttributeTests.CustomTypeWrapper.cs
+
+using NUnit.Framework.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.Attributes
+{
+    #region Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT 
License.
+
+    // Copyright (c) 2021 Charlie Poole, Rob Prouse
+    // 
+    // Permission is hereby granted, free of charge, to any person obtaining a 
copy
+    // of this software and associated documentation files (the "Software"), 
to deal
+    // in the Software without restriction, including without limitation the 
rights
+    // to use, copy, modify, merge, publish, distribute, sublicense, and/or 
sell
+    // copies of the Software, and to permit persons to whom the Software is
+    // furnished to do so, subject to the following conditions:
+    // 
+    // The above copyright notice and this permission notice shall be included 
in
+    // all copies or substantial portions of the Software.
+    // 
+    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
OR
+    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
THE
+    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
IN
+    // THE SOFTWARE.
+
+    #endregion
+
+    public partial class RepeatAttributeTests
+    {
+        private sealed class CustomTypeWrapper : ITypeInfo
+        {
+            private readonly ITypeInfo _baseInfo;
+            private readonly Attribute[] _extraMethodAttributes;
+
+            public CustomTypeWrapper(ITypeInfo baseInfo, Attribute[] 
extraMethodAttributes)
+            {
+                _baseInfo = baseInfo;
+                _extraMethodAttributes = extraMethodAttributes;
+            }
+
+            public IMethodInfo[] GetMethods(BindingFlags flags)
+            {
+                return _baseInfo.GetMethods(flags)
+                    .Select(info => new CustomMethodWrapper(info, 
_extraMethodAttributes))
+                    .ToArray();
+            }
+
+            public Type Type => _baseInfo.Type;
+
+            public ITypeInfo BaseType => _baseInfo.BaseType;
+
+            public string Name => _baseInfo.Name;
+
+            public string FullName => _baseInfo.FullName;
+
+            public Assembly Assembly => _baseInfo.Assembly;
+
+            public string Namespace => _baseInfo.Namespace;
+
+            public bool IsAbstract => _baseInfo.IsAbstract;
+
+            public bool IsGenericType => _baseInfo.IsGenericType;
+
+            public bool ContainsGenericParameters => 
_baseInfo.ContainsGenericParameters;
+
+            public bool IsGenericTypeDefinition => 
_baseInfo.IsGenericTypeDefinition;
+
+            public bool IsSealed => _baseInfo.IsSealed;
+
+            public bool IsStaticClass => _baseInfo.IsStaticClass;
+
+            public object Construct(object[] args)
+            {
+                return _baseInfo.Construct(args);
+            }
+
+            public ConstructorInfo GetConstructor(Type[] argTypes)
+            {
+                return _baseInfo.GetConstructor(argTypes);
+            }
+
+            public T[] GetCustomAttributes<T>(bool inherit) where T : class
+            {
+                return _baseInfo.GetCustomAttributes<T>(inherit);
+            }
+
+            public string GetDisplayName()
+            {
+                return _baseInfo.GetDisplayName();
+            }
+
+            public string GetDisplayName(object[] args)
+            {
+                return _baseInfo.GetDisplayName(args);
+            }
+
+            public Type GetGenericTypeDefinition()
+            {
+                return _baseInfo.GetGenericTypeDefinition();
+            }
+
+            public bool HasConstructor(Type[] argTypes)
+            {
+                return _baseInfo.HasConstructor(argTypes);
+            }
+
+            public bool HasMethodWithAttribute(Type attrType)
+            {
+                return _baseInfo.HasMethodWithAttribute(attrType);
+            }
+
+            public bool IsDefined<T>(bool inherit) where T : class
+            {
+                return _baseInfo.IsDefined<T>(inherit);
+            }
+
+            public bool IsType(Type type)
+            {
+                return _baseInfo.IsType(type);
+            }
+
+            public ITypeInfo MakeGenericType(Type[] typeArgs)
+            {
+                return _baseInfo.MakeGenericType(typeArgs);
+            }
+
+            public IMethodInfo[] GetMethodsWithAttribute<T>(bool inherit) 
where T : class
+            {
+                return _baseInfo.GetMethodsWithAttribute<T>(inherit);
+            }
+        }
+    }
+}
diff --git 
a/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.cs
 
b/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.cs
new file mode 100644
index 000000000..b0409391e
--- /dev/null
+++ 
b/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Attributes/RepeatAttributeTests.cs
@@ -0,0 +1,163 @@
+// Source: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/tests/Attributes/RepeatAttributeTests.cs
+
+using NUnit.Framework;
+using NUnit.Framework.Interfaces;
+using NUnit.Framework.Internal.Builders;
+using NUnit.Framework.Internal.Commands;
+using NUnit.Framework.Internal;
+using System;
+using Lucene.Net.NUnit.TestUtilities;
+using Lucene.Net.TestData.RepeatingTests;
+using System.Linq;
+
+namespace Lucene.Net.Attributes
+{
+    #region Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT 
License.
+
+    // Copyright (c) 2021 Charlie Poole, Rob Prouse
+    // 
+    // Permission is hereby granted, free of charge, to any person obtaining a 
copy
+    // of this software and associated documentation files (the "Software"), 
to deal
+    // in the Software without restriction, including without limitation the 
rights
+    // to use, copy, modify, merge, publish, distribute, sublicense, and/or 
sell
+    // copies of the Software, and to permit persons to whom the Software is
+    // furnished to do so, subject to the following conditions:
+    // 
+    // The above copyright notice and this permission notice shall be included 
in
+    // all copies or substantial portions of the Software.
+    // 
+    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
OR
+    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
THE
+    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
IN
+    // THE SOFTWARE.
+
+    #endregion
+
+    [TestFixture]
+    public partial class RepeatAttributeTests
+    {
+        [TestCase(typeof(RepeatFailOnFirstTryFixture), "Failed(Child)", 1)]
+        [TestCase(typeof(RepeatFailOnSecondTryFixture), "Failed(Child)", 2)]
+        [TestCase(typeof(RepeatFailOnThirdTryFixture), "Failed(Child)", 3)]
+        [TestCase(typeof(RepeatSuccessFixture), "Passed", 3)]
+        [TestCase(typeof(RepeatedTestWithIgnoreAttribute), 
"Skipped:Ignored(Child)", 0)]
+        [TestCase(typeof(RepeatIgnoredOnFirstTryFixture), 
"Skipped:Ignored(Child)", 1)]
+        [TestCase(typeof(RepeatIgnoredOnSecondTryFixture), 
"Skipped:Ignored(Child)", 2)]
+        [TestCase(typeof(RepeatIgnoredOnThirdTryFixture), 
"Skipped:Ignored(Child)", 3)]
+        [TestCase(typeof(RepeatErrorOnFirstTryFixture), "Failed(Child)", 1)]
+        [TestCase(typeof(RepeatErrorOnSecondTryFixture), "Failed(Child)", 2)]
+        [TestCase(typeof(RepeatErrorOnThirdTryFixture), "Failed(Child)", 3)]
+        public void RepeatWorksAsExpected(Type fixtureType, string outcome, 
int nTries)
+        {
+            RepeatingTestsFixtureBase fixture = 
(RepeatingTestsFixtureBase)Reflect.Construct(fixtureType);
+            ITestResult result = TestBuilder.RunTestFixture(fixture);
+
+            Assert.That(result.ResultState.ToString(), Is.EqualTo(outcome));
+            Assert.AreEqual(1, fixture.FixtureSetupCount);
+            Assert.AreEqual(1, fixture.FixtureTeardownCount);
+            Assert.AreEqual(nTries, fixture.SetupCount);
+            Assert.AreEqual(nTries, fixture.TeardownCount);
+            Assert.AreEqual(nTries, fixture.Count);
+        }
+
+        [Test]
+        public void RepeatOnNonLuceneTestCaseSubclass_ShouldFail()
+        {
+            RepeatFailedOnNonLuceneTestCaseSubclass fixture = 
(RepeatFailedOnNonLuceneTestCaseSubclass)Reflect.Construct(typeof(RepeatFailedOnNonLuceneTestCaseSubclass));
+            ITestResult result = TestBuilder.RunTestFixture(fixture);
+            Assert.AreEqual(1, result.FailCount);
+            Assert.IsTrue(result.HasChildren);
+            Assert.AreEqual(1, result.Children.Count());
+            Assert.That(result.Children.Single().Message, 
Is.EqualTo("Lucene.Net.Util.LuceneTestCase+RepeatAttribute may only be used on 
a test in a subclass of LuceneTestCase."));
+            Assert.AreEqual(1, result.Children.Single().FailCount);
+        }
+
+        [Test]
+        public void RepeatUpdatesCurrentRepeatCountPropertyOnEachAttempt()
+        {
+            RepeatingTestsFixtureBase fixture = 
(RepeatingTestsFixtureBase)Reflect.Construct(typeof(RepeatedTestVerifyAttempt));
+            ITestResult result = TestBuilder.RunTestCase(fixture, 
nameof(RepeatedTestVerifyAttempt.PassesTwoTimes));
+
+            Assert.AreEqual(fixture.TearDownResults.Count, fixture.Count + 1, 
"expected the CurrentRepeatCount property to be one less than the number of 
executions");
+            Assert.AreEqual(result.FailCount, 1, "expected that the test 
failed the last repetition");
+        }
+
+        [Test]
+        public void RepeatUpdatesCurrentRepeatCountPropertyOnGreenTest()
+        {
+            RepeatingTestsFixtureBase fixture = 
(RepeatingTestsFixtureBase)Reflect.Construct(typeof(RepeatedTestVerifyAttempt));
+            ITestResult result = TestBuilder.RunTestCase(fixture, 
nameof(RepeatedTestVerifyAttempt.AlwaysPasses));
+
+            Assert.AreEqual(fixture.TearDownResults.Count, fixture.Count + 1, 
"expected the CurrentRepeatCount property to be one less than the number of 
executions");
+            Assert.AreEqual(result.FailCount, 0, "expected that the test 
passes all repetitions without a failure");
+        }
+
+        [Test]
+        public void CategoryWorksWithRepeatedTest()
+        {
+            TestSuite suite = 
TestBuilder.MakeFixture(typeof(RepeatedTestWithCategory));
+            Test test = suite.Tests[0] as Test;
+            System.Collections.IList categories = test.Properties["Category"];
+            Assert.IsNotNull(categories);
+            Assert.AreEqual(1, categories.Count);
+            Assert.AreEqual("SAMPLE", categories[0]);
+        }
+
+        [Test]
+        public void 
NotRunnableWhenIMethodInfoAbstractionReturnsMultipleIRepeatTestAttributes()
+        {
+            var fixtureSuite = new DefaultSuiteBuilder().BuildFrom(new 
CustomTypeWrapper(
+                new 
TypeWrapper(typeof(FixtureWithMultipleRepeatAttributesOnSameMethod)),
+                extraMethodAttributes: new Attribute[]
+                {
+                    new CustomRepeatAttribute(),
+                    new RepeatAttribute(2)
+                }));
+
+            var method = fixtureSuite.Tests.Single();
+
+            Assert.That(method.RunState, Is.EqualTo(RunState.NotRunnable));
+            Assert.That(method.Properties.Get(PropertyNames.SkipReason), 
Is.EqualTo("Multiple attributes that repeat a test may cause issues."));
+        }
+
+        [Test]
+        public void 
IRepeatTestAttributeIsEffectiveWhenAddedThroughIMethodInfoAbstraction()
+        {
+            var fixtureSuite = new DefaultSuiteBuilder().BuildFrom(new 
CustomTypeWrapper(
+                new 
TypeWrapper(typeof(FixtureWithMultipleRepeatAttributesOnSameMethod)),
+                extraMethodAttributes: new Attribute[]
+                {
+                    new RepeatAttribute(2)
+                }));
+
+            var fixtureInstance = new 
FixtureWithMultipleRepeatAttributesOnSameMethod();
+            fixtureSuite.Fixture = fixtureInstance;
+            TestBuilder.RunTest(fixtureSuite, fixtureInstance);
+
+            Assert.That(fixtureInstance.MethodRepeatCount, Is.EqualTo(2));
+        }
+
+        private sealed class FixtureWithMultipleRepeatAttributesOnSameMethod
+        {
+            public int MethodRepeatCount { get; private set; }
+
+            // The IRepeatTest attributes are dynamically applied via 
CustomTypeWrapper.
+            [Test]
+            public void MethodWithMultipleRepeatAttributes()
+            {
+                MethodRepeatCount++;
+            }
+        }
+
+        private sealed class CustomRepeatAttribute : Attribute, IRepeatTest
+        {
+            public TestCommand Wrap(TestCommand command)
+            {
+                throw new NotImplementedException();
+            }
+        }
+    }
+}
diff --git 
a/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Lucene.Net.Tests.TestFramework.NUnitExtensions.csproj
 
b/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Lucene.Net.Tests.TestFramework.NUnitExtensions.csproj
new file mode 100644
index 000000000..597c53c37
--- /dev/null
+++ 
b/src/Lucene.Net.Tests.TestFramework.NUnitExtensions/Lucene.Net.Tests.TestFramework.NUnitExtensions.csproj
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <Import Project="$(SolutionDir)TestTargetFramework.props" />
+
+  <PropertyGroup>
+    
<AssemblyTitle>Lucene.Net.Tests.TestFramework.NUnitExtensions</AssemblyTitle>
+    <RootNamespace>Lucene.Net</RootNamespace>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference 
Include="..\dotnet\Lucene.Net.TestFramework.TestData.NUnit\Lucene.Net.TestFramework.TestData.NUnit.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+    <ProjectReference Include="..\Lucene.Net\Lucene.Net.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+    <ProjectReference 
Include="..\Lucene.Net.Analysis.Common\Lucene.Net.Analysis.Common.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+    <ProjectReference Include="..\Lucene.Net.Codecs\Lucene.Net.Codecs.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+    <ProjectReference 
Include="..\Lucene.Net.TestFramework\Lucene.Net.TestFramework.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+  </ItemGroup>
+
+  <Import Project="$(SolutionDir).build/TestReferences.Common.targets" />
+
+</Project>
+
diff --git 
a/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/Lucene.Net.TestFramework.TestData.NUnit.csproj
 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/Lucene.Net.TestFramework.TestData.NUnit.csproj
new file mode 100644
index 000000000..f66c61e34
--- /dev/null
+++ 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/Lucene.Net.TestFramework.TestData.NUnit.csproj
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    
<TargetFrameworks>net8.0;netstandard2.1;netstandard2.0;net462</TargetFrameworks>
+
+    <AssemblyTitle>Lucene.Net.TestFramework.TestData.NUnit</AssemblyTitle>
+    <RootNamespace>Lucene.Net</RootNamespace>
+
+    <Description>This assembly contains test utilities and data to assist with 
testing custom NUnit extensions.</Description>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\Lucene.Net\Lucene.Net.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+    <ProjectReference 
Include="..\..\Lucene.Net.Analysis.Common\Lucene.Net.Analysis.Common.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+    <ProjectReference 
Include="..\..\Lucene.Net.Codecs\Lucene.Net.Codecs.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+    <ProjectReference 
Include="..\..\Lucene.Net.TestFramework\Lucene.Net.TestFramework.csproj">
+      <SetTargetFramework>$(SetTargetFramework)</SetTargetFramework>
+    </ProjectReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="NUnit" Version="$(NUnitPackageVersion)" />
+  </ItemGroup>
+
+</Project>
+
diff --git 
a/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/RepeatingTests/RepeatedTestFixture.cs
 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/RepeatingTests/RepeatedTestFixture.cs
new file mode 100644
index 000000000..bfd0b2636
--- /dev/null
+++ 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/RepeatingTests/RepeatedTestFixture.cs
@@ -0,0 +1,195 @@
+// Source: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/testdata/RepeatedTestFixture.cs
+
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.TestData.RepeatingTests
+{
+    #region Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT 
License.
+
+    // Copyright (c) 2021 Charlie Poole, Rob Prouse
+    // 
+    // Permission is hereby granted, free of charge, to any person obtaining a 
copy
+    // of this software and associated documentation files (the "Software"), 
to deal
+    // in the Software without restriction, including without limitation the 
rights
+    // to use, copy, modify, merge, publish, distribute, sublicense, and/or 
sell
+    // copies of the Software, and to permit persons to whom the Software is
+    // furnished to do so, subject to the following conditions:
+    // 
+    // The above copyright notice and this permission notice shall be included 
in
+    // all copies or substantial portions of the Software.
+    // 
+    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
OR
+    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
THE
+    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
IN
+    // THE SOFTWARE.
+
+    #endregion
+
+    public class RepeatSuccessFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void RepeatSuccess()
+        {
+            Count++;
+            Assert.IsTrue(true);
+        }
+    }
+
+    public class RepeatFailOnFirstTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void RepeatFailOnFirst()
+        {
+            Count++;
+            Assert.IsFalse(true);
+        }
+    }
+
+    public class RepeatFailOnSecondTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void RepeatFailOnThird()
+        {
+            Count++;
+
+            if (Count == 2)
+                Assert.IsTrue(false);
+        }
+    }
+
+    public class RepeatFailOnThirdTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void RepeatFailOnThird()
+        {
+            Count++;
+
+            if (Count == 3)
+                Assert.IsTrue(false);
+        }
+    }
+
+    public class RepeatedTestWithIgnoreAttribute : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3), Ignore("Ignore this test")]
+        public void RepeatShouldIgnore()
+        {
+            Assert.Fail("Ignored test executed");
+        }
+    }
+
+    public class RepeatIgnoredOnFirstTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void Test()
+        {
+            Count++;
+            Assert.Ignore("Ignoring");
+        }
+    }
+
+    public class RepeatIgnoredOnSecondTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void Test()
+        {
+            Count++;
+
+            if (Count == 2)
+                Assert.Ignore("Ignoring");
+        }
+    }
+
+    public class RepeatIgnoredOnThirdTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void Test()
+        {
+            Count++;
+
+            if (Count == 3)
+                Assert.Ignore("Ignoring");
+        }
+    }
+
+    public class RepeatErrorOnFirstTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void Test()
+        {
+            Count++;
+            throw new Exception("Deliberate Exception");
+        }
+    }
+
+    public class RepeatErrorOnSecondTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void Test()
+        {
+            Count++;
+
+            if (Count == 2)
+                throw new Exception("Deliberate Exception");
+        }
+    }
+
+    public class RepeatErrorOnThirdTryFixture : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void Test()
+        {
+            Count++;
+
+            if (Count == 3)
+                throw new Exception("Deliberate Exception");
+        }
+    }
+
+    public class RepeatedTestWithCategory : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3), Category("SAMPLE")]
+        public void TestWithCategory()
+        {
+            Count++;
+            Assert.IsTrue(true);
+        }
+    }
+
+    public class RepeatedTestVerifyAttempt : RepeatingTestsFixtureBase
+    {
+        [Test, Repeat(3)]
+        public void AlwaysPasses()
+        {
+            Count = TestContext.CurrentContext.CurrentRepeatCount;
+        }
+
+        [Test, Repeat(3)]
+        public void PassesTwoTimes()
+        {
+            Assert.That(Count, 
Is.EqualTo(TestContext.CurrentContext.CurrentRepeatCount), "expected 
CurrentRepeatCount to be incremented only after first two attempts");
+            if (Count > 1)
+            {
+                Assert.Fail("forced failure on 3rd repetition");
+            }
+            Count++;
+        }
+    }
+
+    public class RepeatFailedOnNonLuceneTestCaseSubclass
+    {
+        [Test, Lucene.Net.Util.LuceneTestCase.Repeat(3)]
+        public void AlwaysFails()
+        {
+            // Intentionally empty
+        }
+    }
+}
diff --git 
a/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/RepeatingTests/RepeatingTestsFixtureBase.cs
 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/RepeatingTests/RepeatingTestsFixtureBase.cs
new file mode 100644
index 000000000..482efd3c4
--- /dev/null
+++ 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/RepeatingTests/RepeatingTestsFixtureBase.cs
@@ -0,0 +1,97 @@
+// Source: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/testdata/RepeatingTestsFixtureBase.cs
+
+using Lucene.Net.Util;
+using NUnit.Framework;
+using System.Collections.Generic;
+using Assert = Lucene.Net.TestFramework.Assert;
+
+namespace Lucene.Net.TestData.RepeatingTests
+{
+    #region Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT 
License.
+
+    // Copyright (c) 2021 Charlie Poole, Rob Prouse
+    // 
+    // Permission is hereby granted, free of charge, to any person obtaining a 
copy
+    // of this software and associated documentation files (the "Software"), 
to deal
+    // in the Software without restriction, including without limitation the 
rights
+    // to use, copy, modify, merge, publish, distribute, sublicense, and/or 
sell
+    // copies of the Software, and to permit persons to whom the Software is
+    // furnished to do so, subject to the following conditions:
+    // 
+    // The above copyright notice and this permission notice shall be included 
in
+    // all copies or substantial portions of the Software.
+    // 
+    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
OR
+    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
THE
+    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
IN
+    // THE SOFTWARE.
+
+    #endregion
+
+    [TestFixture]
+    public class RepeatingTestsFixtureBase : LuceneTestCase
+    {
+        private int fixtureSetupCount;
+        private int fixtureTeardownCount;
+        private int setupCount;
+        private int teardownCount;
+        private readonly List<string> tearDownResults = new List<string>();
+        private long randomSeed;
+        private long testSeed;
+
+        [OneTimeSetUp]
+        public override void OneTimeSetUp()
+        {
+            base.OneTimeSetUp();
+            fixtureSetupCount++;
+        }
+
+        [OneTimeTearDown]
+        public override void OneTimeTearDown()
+        {
+            fixtureTeardownCount++;
+            base.OneTimeTearDown();
+        }
+
+        [SetUp]
+        public override void SetUp()
+        {
+            base.SetUp();
+            setupCount++;
+            var randomizedContext = RandomizedContext.CurrentContext;
+            Assert.IsNotNull(randomizedContext);
+            Assert.AreNotEqual(testSeed, randomizedContext.TestSeed, 
"RandomizedContext.TestSeed must change for each iteration.");
+            randomSeed = randomizedContext.RandomSeed;
+            testSeed = randomizedContext.TestSeed;
+            Assert.AreEqual(testSeed, 
((J2N.Randomizer)randomizedContext.RandomGenerator).Seed, 
"RandomizedContext.RandomGenerator must have the same seed as 
RandomizedContext.TestSeed.");
+        }
+
+        [TearDown]
+        public override void TearDown()
+        {
+            
tearDownResults.Add(TestContext.CurrentContext.Result.Outcome.ToString());
+            teardownCount++;
+            var randomizedContext = RandomizedContext.CurrentContext;
+            Assert.IsNotNull(randomizedContext);
+            Assert.AreEqual(randomSeed, randomizedContext.RandomSeed, 
"RandomizedContext.RandomSeed must be the same between StartUp() and 
TearDown().");
+            Assert.AreEqual(testSeed, randomizedContext.TestSeed, 
"RandomizedContext.TestSeed must be the same between StartUp() and 
TearDown().");
+            Assert.AreEqual(testSeed, 
((J2N.Randomizer)randomizedContext.RandomGenerator).Seed, 
"RandomizedContext.RandomGenerator must have the same seed as 
RandomizedContext.TestSeed.");
+            base.TearDown();
+        }
+
+        public int FixtureSetupCount => fixtureSetupCount;
+
+        public int FixtureTeardownCount => fixtureTeardownCount;
+
+        public int SetupCount => setupCount;
+
+        public int TeardownCount => teardownCount;
+
+        public List<string> TearDownResults => tearDownResults;
+
+        public int Count { get; protected set; }
+    }
+}
diff --git 
a/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/TestUtilities/TestBuilder.cs
 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/TestUtilities/TestBuilder.cs
new file mode 100644
index 000000000..3e299eee7
--- /dev/null
+++ 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/TestUtilities/TestBuilder.cs
@@ -0,0 +1,260 @@
+// Source: 
https://github.com/nunit/nunit/blob/v3.14.0/src/NUnitFramework/TestBuilder.cs
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using NUnit.Framework;
+using NUnit.Compatibility;
+using NUnit.Framework.Interfaces;
+using NUnit.Framework.Internal.Builders;
+using NUnit.Framework.Internal;
+using NUnit.Framework.Internal.Commands;
+using NUnit.Framework.Internal.Execution;
+using System.Linq;
+using Lucene.Net.Search;
+using Lucene.Net.Util;
+
+namespace Lucene.Net.NUnit.TestUtilities
+{
+    #region Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT 
License.
+
+    // Copyright (c) 2021 Charlie Poole, Rob Prouse
+    // 
+    // Permission is hereby granted, free of charge, to any person obtaining a 
copy
+    // of this software and associated documentation files (the "Software"), 
to deal
+    // in the Software without restriction, including without limitation the 
rights
+    // to use, copy, modify, merge, publish, distribute, sublicense, and/or 
sell
+    // copies of the Software, and to permit persons to whom the Software is
+    // furnished to do so, subject to the following conditions:
+    // 
+    // The above copyright notice and this permission notice shall be included 
in
+    // all copies or substantial portions of the Software.
+    // 
+    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
OR
+    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
THE
+    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
IN
+    // THE SOFTWARE.
+
+    #endregion
+
+    /// <summary>
+    /// Utility Class used to build and run NUnit tests used as test data
+    /// </summary>
+    public static class TestBuilder
+    {
+        #region Build Tests
+
+        public static TestSuite MakeSuite(string name)
+        {
+            return new TestSuite(name);
+        }
+
+        public static TestSuite MakeFixture(Type type)
+        {
+            //return (TestSuite)new DefaultSuiteBuilder().BuildFrom(new 
TypeWrapper(type));
+            TestSuite suite = (TestSuite)new 
DefaultSuiteBuilder().BuildFrom(new TypeWrapper(type));
+
+            // LUCENENET: We wrapped our TestSuite in another TestSuite
+            // (SetUpFixture), so we need to get the child test (the class).
+            if (suite is not null && 
suite.TypeInfo.Type.Equals(typeof(LuceneTestCase.SetUpFixture)))
+            {
+                suite = (TestSuite)suite.Tests[0];
+            }
+            return suite;
+        }
+
+        public static TestSuite MakeFixture(object fixture)
+        {
+            TestSuite suite = MakeFixture(fixture.GetType());
+            suite.Fixture = fixture;
+            return suite;
+        }
+
+        public static TestSuite MakeParameterizedMethodSuite(Type type, string 
methodName)
+        {
+            var suite = MakeTestFromMethod(type, methodName) as TestSuite;
+            Assert.NotNull(suite, "Unable to create parameterized suite - most 
likely there is no data provided");
+            return suite;
+        }
+
+        public static TestMethod MakeTestCase(Type type, string methodName)
+        {
+            var test = MakeTestFromMethod(type, methodName) as TestMethod;
+            Assert.NotNull(test, "Unable to create TestMethod from {0}", 
methodName);
+
+            return test;
+        }
+
+        // Will return either a ParameterizedMethodSuite or an NUnitTestMethod
+        // depending on whether the method takes arguments or not
+        internal static Test MakeTestFromMethod(Type type, string methodName)
+        {
+            var method = type.GetMethod(methodName, BindingFlags.Static | 
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+
+            if (method == null)
+                Assert.Fail("Method not found: " + methodName);
+            var test = new DefaultTestCaseBuilder().BuildFrom(new 
MethodWrapper(type, method));
+            // LUCENENET: We need to initialize the RandomizedContext for our 
custom attributes to work.
+            InitializeTestFixture(test);
+            return test;
+        }
+
+        internal static void InitializeTestFixture(Test fixture)
+        {
+            var random = new J2N.Randomizer(); // LUCENENET TOOD: Initilize 
this to make it repeatable?
+            long initialSeed = random.NextInt64();
+            long testSeed = random.NextInt64();
+            var randomizedContext = new RandomizedContext(fixture, 
fixture.TypeInfo.Assembly, initialSeed, testSeed);
+            _ = new 
LuceneRandomSeedInitializer().InitializeTestFixture(fixture, randomizedContext);
+        }
+
+        #endregion
+
+        #region Create WorkItems
+
+        public static WorkItem CreateWorkItem(Type type)
+        {
+            return CreateWorkItem(MakeFixture(type));
+        }
+
+        public static WorkItem CreateWorkItem(Type type, string methodName)
+        {
+            return CreateWorkItem(MakeTestFromMethod(type, methodName));
+        }
+
+        public static WorkItem CreateWorkItem(Test test)
+        {
+            var context = new TestExecutionContext();
+            context.Dispatcher = new SuperSimpleDispatcher();
+
+            return CreateWorkItem(test, context);
+        }
+
+        public static WorkItem CreateWorkItem(Test test, object testObject)
+        {
+            var context = new TestExecutionContext();
+            context.TestObject = testObject;
+            context.Dispatcher = new SuperSimpleDispatcher();
+
+            return CreateWorkItem(test, context);
+        }
+
+        public static WorkItem CreateWorkItem(Test test, TestExecutionContext 
context)
+        {
+            // LUCENENET: IDebugger is internal, so our only options are to 
use this obsolete method or to use Reflection.
+#pragma warning disable CS0618 // Type or member is obsolete
+            var work = WorkItemBuilder.CreateWorkItem(test, TestFilter.Empty, 
true);
+#pragma warning restore CS0618 // Type or member is obsolete
+            work.InitializeContext(context);
+
+            return work;
+        }
+
+        #endregion
+
+        #region Run Tests
+
+        public static ITestResult RunTestFixture(Type type)
+        {
+            return RunTest(MakeFixture(type), null);
+        }
+
+        public static ITestResult RunTestFixture(object fixture)
+        {
+            return RunTest(MakeFixture(fixture), fixture);
+        }
+
+        public static ITestResult RunParameterizedMethodSuite(Type type, 
string methodName)
+        {
+            var suite = MakeParameterizedMethodSuite(type, methodName);
+
+            object testObject = null;
+            if (!type.IsStatic())
+                testObject = Reflect.Construct(type);
+
+            return RunTest(suite, testObject);
+        }
+
+        public static ITestResult RunTestCase(Type type, string methodName)
+        {
+            var testMethod = MakeTestCase(type, methodName);
+
+            object testObject = null;
+            if (!type.IsStatic())
+                testObject = Reflect.Construct(type);
+
+            return RunTest(testMethod, testObject);
+        }
+
+        public static ITestResult RunTestCase(object fixture, string 
methodName)
+        {
+            var testMethod = MakeTestCase(fixture.GetType(), methodName);
+
+            return RunTest(testMethod, fixture);
+        }
+
+        public static ITestResult RunAsTestCase(Action action)
+        {
+            var method = action.GetMethodInfo();
+            var testMethod = MakeTestCase(method.DeclaringType, method.Name);
+            return RunTest(testMethod);
+        }
+
+        public static ITestResult RunTest(Test test)
+        {
+            return RunTest(test, null);
+        }
+
+        public static ITestResult RunTest(Test test, object testObject)
+        {
+            return ExecuteWorkItem(CreateWorkItem(test, testObject));
+        }
+
+        public static ITestResult ExecuteWorkItem(WorkItem work)
+        {
+            work.Execute();
+
+            // TODO: Replace with an event - but not while method is static
+            while (work.State != WorkItemState.Complete)
+            {
+                Thread.Sleep(1);
+            }
+
+            return work.Result;
+        }
+
+        #endregion
+
+        #region Nested TestDispatcher Class
+
+        /// <summary>
+        /// SuperSimpleDispatcher merely executes the work item.
+        /// It is needed particularly when running suites, since
+        /// the child items require a dispatcher in the context.
+        /// </summary>
+        class SuperSimpleDispatcher : IWorkItemDispatcher
+        {
+            public int LevelOfParallelism { get { return 0; } }
+
+            public void Start(WorkItem topLevelWorkItem)
+            {
+                topLevelWorkItem.Execute();
+            }
+
+            public void Dispatch(WorkItem work)
+            {
+                work.Execute();
+            }
+
+            public void CancelRun(bool force)
+            {
+                throw new NotImplementedException();
+            }
+        }
+        #endregion
+    }
+}
diff --git 
a/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/TypeExtensions.cs 
b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/TypeExtensions.cs
new file mode 100644
index 000000000..7d1660513
--- /dev/null
+++ b/src/dotnet/Lucene.Net.TestFramework.TestData.NUnit/TypeExtensions.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Lucene.Net
+{
+    #region Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT 
License.
+
+    // Copyright (c) 2021 Charlie Poole, Rob Prouse
+    // 
+    // Permission is hereby granted, free of charge, to any person obtaining a 
copy
+    // of this software and associated documentation files (the "Software"), 
to deal
+    // in the Software without restriction, including without limitation the 
rights
+    // to use, copy, modify, merge, publish, distribute, sublicense, and/or 
sell
+    // copies of the Software, and to permit persons to whom the Software is
+    // furnished to do so, subject to the following conditions:
+    // 
+    // The above copyright notice and this permission notice shall be included 
in
+    // all copies or substantial portions of the Software.
+    // 
+    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
OR
+    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
THE
+    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
IN
+    // THE SOFTWARE.
+
+    #endregion
+
+    internal static class TypeExtensions
+    {
+        public static bool IsStatic(this Type type)
+        {
+            return type.IsAbstract && type.IsSealed;
+        }
+    }
+}

Reply via email to