浏览代码

Fixes #3930 - Splits tests to `Tests/UnitTests`, `Tests/IntegrationTests`, `Tests/StressTests` (#3954)

* Tons of API doc updates

* Removed stale test

* Removed stale tests

* Fixed Skipped Shadow test 1

* Fixed Skipped Shadow test 2

* Fixed Skipped Shadow test 3

* Removed stale test

* Removed stale test2

* Explicit unregister of event handler on Application.Driver!.ClearedContents

* Added Toplevels to dict

* code cleanup

* spelling error

* Removed stale test3

* Removed stale test4

* Removed stale test5

* added script

* tweaked script

* tweaked script

* Created StressTests project; moved some tests

* Created IntegrationTests project; moved some tests

* New yml

* made old yml just unit tests

* Tweaked Button_IsDefault_Raises_Accepted_Correctly

* tweaked script

* cleaned up ymls

* tweakled up ymls

* stress tests...

* stress tests on ubuntu only

* Fixed WindowsDriver in InvokeLeakTest

* Fixed WindowsDriver in InvokeLeakTest2

* Added Directory.Packages.props.
Added Directory.Build.props

* Shortened StressTest time

* Removed dupe file.

* DemoFiles

* Moved all tests to ./Tests dir.

* Fixed release build issue

* Fixed .sln file

* Fixed .sl* files

* Fixing ymls

* Fixing interation tests

* Create link to the file TestHelpers.

* Created Tests/UnitTestsParallelizable.
Moved all obviously parallelizable tests.
Updated yml.

* fixing logs

* fixing logs2

* fixing logs3

* don't require stress to pass for PRs

* Fix a failure?

* tweaked script

* Coudl this be it?

* Moved tons of tests to parallelizable

* Fixed some stuff

* Script to find duplicate tests

* Testing workflows

* Updated to v4

* Fix RelativeBasePath issue

* Replace powershell to pwsh

* Add ignore projects.

* Removed dupe unit tests

* Code cleanup of tests

* Cleaned up test warnings

* yml tweak

* Moved setter

* tweak ymls

* just randomly throwing spaghetti at a wall

* Enable runing 5 test runners in par

* Turned off DEBUG_DISPOSABLE for par tests

* RunningUnitTests=true

* code cleanup (forcing more Action runs)

* DISABLE_DEBUG_IDISPOSABLE

* Added View.DebugIDisposable. False by default.

* Remobed bogus tareet

* Remobed bogus tareet2

* fixed warning

* added api doc

* fixed warning

* fixed warning

* fixed warning2

* fixed warning3

* fixed warning4

---------

Co-authored-by: BDisp <[email protected]>
Tig 4 月之前
父节点
当前提交
b0f32811eb
共有 100 个文件被更改,包括 1570 次插入903 次删除
  1. 34 0
      .github/workflows/build-release.yml
  2. 14 0
      .github/workflows/check-duplicates.yml
  3. 0 119
      .github/workflows/dotnet-core.yml
  4. 60 0
      .github/workflows/integration-tests.yml
  5. 51 0
      .github/workflows/stress-tests.yml
  6. 116 0
      .github/workflows/unit-tests.yml
  7. 2 0
      .gitignore
  8. 1 3
      Benchmarks/Benchmarks.csproj
  9. 2 4
      CommunityToolkitExample/CommunityToolkitExample.csproj
  10. 0 0
      DemoFiles/example_config.json
  11. 11 0
      Directory.Build.props
  12. 52 0
      Directory.Packages.props
  13. 0 1
      Example/Example.csproj
  14. 12 13
      GitVersion.yml
  15. 1 3
      NativeAot/NativeAot.csproj
  16. 5 2
      NoSamples.slnf
  17. 3 4
      ReactiveExample/ReactiveExample.csproj
  18. 4 1
      Release.slnf
  19. 二进制
      Scripts/.testloop.sh.swp
  20. 92 0
      Scripts/FindDuplicateTestMethodsInSameFileName.ps1
  21. 50 0
      Scripts/FindDuplicateTests.ps1
  22. 30 0
      Scripts/testloop.sh
  23. 1 3
      SelfContained/SelfContained.csproj
  24. 6 3
      Terminal.Gui/Application/Application.Mouse.cs
  25. 35 27
      Terminal.Gui/Application/Application.Run.cs
  26. 3 3
      Terminal.Gui/Application/Application.cs
  27. 4 1
      Terminal.Gui/Application/ApplicationImpl.cs
  28. 11 2
      Terminal.Gui/Application/RunState.cs
  29. 3 0
      Terminal.Gui/Configuration/ConfigurationManager.cs
  30. 1 1
      Terminal.Gui/Configuration/ScopeJsonConverter.cs
  31. 6 1
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs
  32. 175 166
      Terminal.Gui/Terminal.Gui.csproj
  33. 1 1
      Terminal.Gui/View/DrawContext.cs
  34. 2 2
      Terminal.Gui/View/DrawEventArgs.cs
  35. 1 0
      Terminal.Gui/View/View.Mouse.cs
  36. 87 65
      Terminal.Gui/View/View.cs
  37. 2 0
      Terminal.Gui/Views/Bar.cs
  38. 2 2
      Terminal.Gui/Views/Dialog.cs
  39. 2 0
      Terminal.Gui/Views/Line.cs
  40. 2 0
      Terminal.Gui/Views/RadioGroup.cs
  41. 2 0
      Terminal.Gui/Views/ScrollBar/ScrollBar.cs
  42. 1 1
      Terminal.Gui/Views/Wizard/WizardStep.cs
  43. 31 7
      Terminal.sln
  44. 2 0
      Terminal.sln.DotSettings
  45. 40 0
      Tests/IntegrationTests/IntegrationTests.csproj
  46. 7 165
      Tests/IntegrationTests/UICatalog/ScenarioTests.cs
  47. 0 0
      Tests/IntegrationTests/xunit.runner.json
  48. 29 0
      Tests/README.md
  49. 111 0
      Tests/StressTests/ApplicationStressTests.cs
  50. 193 0
      Tests/StressTests/ScenariosStressTests.cs
  51. 44 0
      Tests/StressTests/StressTests.csproj
  52. 6 0
      Tests/StressTests/xunit.runner.json
  53. 0 0
      Tests/UnitTests/Application/Application.NavigationTests.cs
  54. 16 5
      Tests/UnitTests/Application/ApplicationScreenTests.cs
  55. 2 47
      Tests/UnitTests/Application/ApplicationTests.cs
  56. 2 1
      Tests/UnitTests/Application/CursorTests.cs
  57. 4 116
      Tests/UnitTests/Application/KeyboardTests.cs
  58. 28 120
      Tests/UnitTests/Application/MainLoopTests.cs
  59. 0 0
      Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs
  60. 2 1
      Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs
  61. 2 0
      Tests/UnitTests/Application/RunStateTests.cs
  62. 0 0
      Tests/UnitTests/Application/StackExtensionsTests.cs
  63. 2 0
      Tests/UnitTests/Application/SynchronizatonContextTests.cs
  64. 0 0
      Tests/UnitTests/AssemblyInfo.cs
  65. 140 0
      Tests/UnitTests/AutoInitShutdownAttribute.cs
  66. 0 0
      Tests/UnitTests/Clipboard/ClipboardTests.cs
  67. 1 0
      Tests/UnitTests/Configuration/AppScopeTests.cs
  68. 1 0
      Tests/UnitTests/Configuration/AttributeJsonConverterTests.cs
  69. 0 0
      Tests/UnitTests/Configuration/ColorJsonConverterTests.cs
  70. 1 0
      Tests/UnitTests/Configuration/ColorSchemeJsonConverterTests.cs
  71. 0 0
      Tests/UnitTests/Configuration/ConfigPropertyTests.cs
  72. 1 0
      Tests/UnitTests/Configuration/ConfigurationMangerTests.cs
  73. 0 0
      Tests/UnitTests/Configuration/GlyphTests.cs
  74. 0 0
      Tests/UnitTests/Configuration/KeyCodeJsonConverterTests.cs
  75. 0 0
      Tests/UnitTests/Configuration/KeyJsonConverterTests.cs
  76. 0 0
      Tests/UnitTests/Configuration/RuneJsonConverterTests.cs
  77. 0 0
      Tests/UnitTests/Configuration/SerializableConfigurationPropertyTests.cs
  78. 2 1
      Tests/UnitTests/Configuration/SettingsScopeTests.cs
  79. 1 0
      Tests/UnitTests/Configuration/ThemeScopeTests.cs
  80. 1 0
      Tests/UnitTests/Configuration/ThemeTests.cs
  81. 4 4
      Tests/UnitTests/ConsoleDrivers/AddRuneTests.cs
  82. 0 0
      Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs
  83. 0 0
      Tests/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs
  84. 0 0
      Tests/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs
  85. 0 0
      Tests/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs
  86. 0 0
      Tests/UnitTests/ConsoleDrivers/ClipRegionTests.cs
  87. 2 2
      Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
  88. 0 0
      Tests/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs
  89. 0 0
      Tests/UnitTests/ConsoleDrivers/ConsoleScrolllingTests.cs
  90. 8 6
      Tests/UnitTests/ConsoleDrivers/ContentsTests.cs
  91. 0 0
      Tests/UnitTests/ConsoleDrivers/DriverColorTests.cs
  92. 0 0
      Tests/UnitTests/ConsoleDrivers/KeyCodeTests.cs
  93. 0 0
      Tests/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs
  94. 0 0
      Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs
  95. 0 0
      Tests/UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs
  96. 0 0
      Tests/UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs
  97. 0 0
      Tests/UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs
  98. 0 0
      Tests/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs
  99. 0 0
      Tests/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs
  100. 0 0
      Tests/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs

+ 34 - 0
.github/workflows/build-release.yml

@@ -0,0 +1,34 @@
+name: Ensure that Release builds are not broken
+
+on:
+  push:
+    branches: [ v2_release, v2_develop ]
+    paths-ignore:
+      - '**.md'
+  pull_request:
+    branches: [ v2_release, v2_develop ]
+    paths-ignore:
+      - '**.md'
+      
+jobs:
+  build_release:
+    # Ensure that RELEASE builds are not broken
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v4
+
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v4
+      with:
+        dotnet-version: 8.x
+        dotnet-quality: 'ga'
+
+    - name: Build Release Terminal.Gui
+      run: dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release
+
+    - name: Pack Release Terminal.Gui
+      run: dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages
+
+    - name: Build Release Solution
+      run: dotnet build --configuration Release

+ 14 - 0
.github/workflows/check-duplicates.yml

@@ -0,0 +1,14 @@
+name: Check for Duplicate UnitTests
+on:
+  push:
+    branches: [ v2_release, v2_develop ]
+  pull_request:
+    branches: [ v2_release, v2_develop ]
+  workflow_dispatch:
+jobs:
+  check-duplicates:
+    runs-on: windows-latest
+    steps:
+    - uses: actions/checkout@v4
+    - name: Run Duplicate Test Check
+      run: pwsh -File ./Scripts/FindDuplicateTestMethodsInSameFileName.ps1 -solutionPath "$PWD"

+ 0 - 119
.github/workflows/dotnet-core.yml

@@ -1,119 +0,0 @@
-name: Build & Test Terminal.Gui with .NET Core
-
-on:
-  push:
-    branches: [ v2_release, v2_develop ]
-    paths-ignore:
-      - '**.md'
-  pull_request:
-    branches: [ v2_release, v2_develop ]
-    paths-ignore:
-      - '**.md'
-      
-jobs:
-  build_and_test_debug:
-
-    runs-on: ${{ matrix.os }}
-    strategy:
-      # Turn off fail-fast to let all runners run even if there are errors
-      fail-fast: true
-      matrix:
-        os: [ ubuntu-latest, windows-latest, macos-latest ]
-
-    timeout-minutes: 10
-    steps:
-
-# Build (Debug)
-
-    - name: Checkout code
-      uses: actions/checkout@v4
-
-    - name: Setup .NET Core
-      uses: actions/setup-dotnet@v4
-      with:
-        dotnet-version: 8.x
-        dotnet-quality: 'ga'
-
-    - name: Install dependencies
-      run: |
-        dotnet restore
-
-    - name: Build Debug
-      run: dotnet build --configuration Debug --no-restore
-
-# Test
-    # Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac
-    # See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed.
-    # Without it, the test runner crashes on ubuntu (but not Windows or mac)
-
-    - name: MacOS - Patch test runner settings to stop on fail
-      if: runner.os == 'macOS'
-      run: |
-        brew install gnu-sed
-        gsed -i 's/"stopOnFail": false/"stopOnFail": true/g' UnitTests/xunit.runner.json
-
-    - name: Windows/Linux - Patch test runner settings to stop on fail
-      if: runner.os != 'macOS'
-      run: |
-          sed -i 's/"stopOnFail": false/"stopOnFail": true/g' UnitTests/xunit.runner.json
-
-    - name: Set VSTEST_DUMP_PATH
-      shell: bash
-      run: echo "{VSTEST_DUMP_PATH}={logs/${{ runner.os }}/}" >> $GITHUB_ENV
-
-    - name: Test
-      run: |
-       dotnet test --verbosity normal --collect:"XPlat Code Coverage" --settings UnitTests/coverlet.runsettings --diag:logs/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always 
-     
-       # mv -v UnitTests/TestResults/*/*.* UnitTests/TestResults/
-
-    - name: Upload Test Logs
-      if: always()
-      uses: actions/upload-artifact@v4
-      with:
-        name: test-logs-${{ runner.os }}
-        path: |
-          logs/    
-          UnitTests/TestResults/
-  
-
-  build_release:
-    # Ensure that RELEASE builds are not broken
-    runs-on: ubuntu-latest
-    steps:
-    - name: Checkout code
-      uses: actions/checkout@v4
-
-    - name: Setup .NET Core
-      uses: actions/setup-dotnet@v4
-      with:
-        dotnet-version: 8.x
-        dotnet-quality: 'ga'
-
-    - name: Build Release Terminal.Gui
-      run: dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release
-
-    - name: Pack Release Terminal.Gui
-      run: dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages
-
-    - name: Build Release Solution
-      run: dotnet build --configuration Release
-
-
-    # Note: this step is currently not writing to the gist for some reason
-    # - name: Create Test Coverage Badge
-    #   uses: simon-k/[email protected]
-    #   id: create_coverage_badge
-    #   with:
-    #     label: Unit Test Coverage
-    #     color: brightgreen
-    #     path: UnitTests/TestResults/coverage.opencover.xml
-    #     gist-filename: code-coverage.json
-    #     # https://gist.github.com/migueldeicaza/90ef67a684cb71db1817921a970f8d27
-    #     gist-id: 90ef67a684cb71db1817921a970f8d27
-    #     gist-auth-token: ${{ secrets.GIST_AUTH_TOKEN }}   
-
-    # - name: Print Code Coverage
-    #   run: |
-    #     echo "Code coverage percentage: ${{steps.create_coverage_badge.outputs.percentage}}%"
-    #     echo "Badge data: ${{steps.create_coverage_badge.outputs.badge}}"

+ 60 - 0
.github/workflows/integration-tests.yml

@@ -0,0 +1,60 @@
+name: Build & Run Integration Tests
+
+on:
+  push:
+    branches: [ v2_release, v2_develop ]
+    paths-ignore:
+      - '**.md'
+  pull_request:
+    branches: [ v2_release, v2_develop ]
+    paths-ignore:
+      - '**.md'
+      
+jobs:
+  build_and_test_debug:
+
+    runs-on: ${{ matrix.os }}
+    strategy:
+      # Turn off fail-fast to let all runners run even if there are errors
+      fail-fast: true
+      matrix:
+        os: [ ubuntu-latest, windows-latest, macos-latest ]
+
+    timeout-minutes: 10
+    steps:
+
+    - name: Checkout code
+      uses: actions/checkout@v4
+
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v4
+      with:
+        dotnet-version: 8.x
+        dotnet-quality: 'ga'
+
+    - name: Install dependencies
+      run: |
+        dotnet restore
+
+    - name: Build IntegrationTests
+      run: dotnet build Tests/IntegrationTests --configuration Debug --no-restore
+
+    - name: Set VSTEST_DUMP_PATH
+      shell: bash
+      run: echo "{VSTEST_DUMP_PATH}={logs/${{ runner.os }}/}" >> $GITHUB_ENV
+
+    - name: Run IntegrationTests
+      run: |
+       dotnet test Tests/IntegrationTests --no-build --verbosity normal --diag:logs/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=true
+     
+       # mv -v Tests/IntegrationTests/TestResults/*/*.* TestResults/IntegrationTests/
+
+    - name: Upload Test Logs
+      if: always()
+      uses: actions/upload-artifact@v4
+      with:
+        name: integration-test-logs-${{ runner.os }}
+        path: |
+          logs/    
+          TestResults/IntegrationTests/
+

+ 51 - 0
.github/workflows/stress-tests.yml

@@ -0,0 +1,51 @@
+name: Run StressTests (for 15 minutes)
+
+on:
+  schedule:
+    - cron: '0 0 * * *' # Runs every day at midnight UTC
+  push:
+    branches: [ v2_release, v2_develop ]
+    paths-ignore:
+      - '**.md'
+      
+jobs:
+  run_stress_tests:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ ubuntu-latest ]
+
+    timeout-minutes: 70 # Allow some buffer time beyond the 1-hour test duration
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Setup .NET Core
+        uses: actions/setup-dotnet@v4
+        with:
+          dotnet-version: 8.x
+          dotnet-quality: 'ga'
+
+      - name: Install dependencies
+        run: dotnet restore
+
+      - name: Build StressTests
+        run: dotnet build Tests/StressTests --configuration Debug --no-restore
+
+      - name: Run StressTests for 15 minutes
+        run: |
+          end=$((SECONDS+900))
+          while [ $SECONDS -lt $end ]; do
+            dotnet test Tests/StressTests --no-build --verbosity normal --diag:logs/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=true
+          done
+
+      - name: Upload Test Logs
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: stress-test-logs-${{ runner.os }}
+          path: |
+            logs/
+            TestResults/StressTests
+

+ 116 - 0
.github/workflows/unit-tests.yml

@@ -0,0 +1,116 @@
+name: Build & Run Unit Tests
+
+on:
+  push:
+    branches: [ v2_release, v2_develop ]
+    paths-ignore:
+      - '**.md'
+  pull_request:
+    branches: [ v2_release, v2_develop ]
+    paths-ignore:
+      - '**.md'
+      
+jobs:
+  non_parallel_unittests:
+    name: Non-Parallel Unit Tests  
+    runs-on: ${{ matrix.os }}
+    strategy:
+      # Turn off fail-fast to let all runners run even if there are errors
+      fail-fast: true
+      matrix:
+        os: [ ubuntu-latest, windows-latest, macos-latest ]
+
+    timeout-minutes: 10
+    steps:
+
+    - name: Checkout code
+      uses: actions/checkout@v4
+
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v4
+      with:
+        dotnet-version: 8.x
+        dotnet-quality: 'ga'
+
+    - name: Install dependencies
+      run: |
+        dotnet restore
+
+    - name: Build Solution Debug
+      run: dotnet build --configuration Debug --no-restore
+
+# Test
+    # Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac
+    # See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed.
+    # Without it, the test runner crashes on ubuntu (but not Windows or mac)
+
+    - name: Set VSTEST_DUMP_PATH
+      shell: bash
+      run: echo "{VSTEST_DUMP_PATH}={logs/UnitTests/${{ runner.os }}/}" >> $GITHUB_ENV
+
+    - name: Run UnitTests
+      run: |
+       dotnet test Tests/UnitTests --no-build --verbosity normal --collect:"XPlat Code Coverage" --settings Tests/UnitTests/coverlet.runsettings --diag:logs/UnitTests/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=true
+     
+       # mv -v Tests/UnitTests/TestResults/*/*.* TestResults/UnitTests/
+
+    - name: Upload Test Logs
+      if: always()
+      uses: actions/upload-artifact@v4
+      with:
+        name: non_parallel_unittests-logs-${{ runner.os }}
+        path: |
+          logs/UnitTests    
+          TestResults/UnitTests/
+  
+  parallel_unittests:
+    name: Parallel Unit Tests  
+    runs-on: ${{ matrix.os }}
+    strategy:
+      # Turn off fail-fast to let all runners run even if there are errors
+      fail-fast: true
+      matrix:
+        os: [ ubuntu-latest, windows-latest, macos-latest ]
+
+    timeout-minutes: 10
+    steps:
+
+    - name: Checkout code
+      uses: actions/checkout@v4
+
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v4
+      with:
+        dotnet-version: 8.x
+        dotnet-quality: 'ga'
+
+    - name: Install dependencies
+      run: |
+        dotnet restore
+
+    - name: Build Solution Debug
+      run: dotnet build --configuration Debug --no-restore
+
+# Test
+    # Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac
+    # See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed.
+    # Without it, the test runner crashes on ubuntu (but not Windows or mac)
+
+    - name: Set VSTEST_DUMP_PATH
+      shell: bash
+      run: echo "{VSTEST_DUMP_PATH}={logs/UnitTestsParallelizable/${{ runner.os }}/}" >> $GITHUB_ENV
+
+    - name: Run UnitTestsParallelizable
+      run: |
+       dotnet test Tests/UnitTestsParallelizable --no-build --verbosity normal --collect:"XPlat Code Coverage" --settings Tests/UnitTestsParallelizable/coverlet.runsettings --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=true
+     
+       # mv -v Tests/UnitTestsParallelizable/TestResults/*/*.* TestResults/UnitTestsParallelizable/
+
+    - name: Upload UnitTestsParallelizable Logs
+      if: always()
+      uses: actions/upload-artifact@v4
+      with:
+        name: parallel_unittests-logs-${{ runner.os }}
+        path: |
+          logs/UnitTestsParallelizable/
+          TestResults/UnitTestsParallelizable/

+ 2 - 0
.gitignore

@@ -60,3 +60,5 @@ demo.*
 *.dotCover
 
 logs/
+
+log.*

+ 1 - 3
Benchmarks/Benchmarks.csproj

@@ -2,15 +2,13 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0</TargetFramework>
     <IsPackable>false</IsPackable>
-    <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <RootNamespace>Terminal.Gui.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
+    <PackageReference Include="BenchmarkDotNet" />
   </ItemGroup>
 
   <ItemGroup>

+ 2 - 4
CommunityToolkitExample/CommunityToolkitExample.csproj

@@ -2,14 +2,12 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0</TargetFramework>
-    <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="CommunityToolkit.Mvvm" Version="[8.2.2,9)" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="[8,9)" />
+    <PackageReference Include="CommunityToolkit.Mvvm" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
     <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
   </ItemGroup>
 

+ 0 - 0
example_config.json → DemoFiles/example_config.json


+ 11 - 0
Directory.Build.props

@@ -0,0 +1,11 @@
+<Project>
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <!--<Nullable>enable</Nullable>-->
+        <LangVersion>12</LangVersion>
+    </PropertyGroup>
+    <PropertyGroup>
+        <DisableDebugIDisposable>false</DisableDebugIDisposable>
+    </PropertyGroup>
+</Project>

+ 52 - 0
Directory.Packages.props

@@ -0,0 +1,52 @@
+<Project>
+	<PropertyGroup>
+		<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
+	</PropertyGroup>
+	<ItemGroup>
+		<!-- Enable Nuget Source Link for github -->
+		<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[8,9)" />
+
+		<PackageVersion Include="ColorHelper" Version="[1.8.1,2)" />
+		<PackageVersion Include="JetBrains.Annotations" Version="[2024.2.0,)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.10,5)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.10,5)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.10,5)" />
+		<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
+		<PackageVersion Include="System.IO.Abstractions" Version="[21.0.22,22)" />
+		<PackageVersion Include="System.Text.Json" Version="[8.0.5,9)" />
+		<PackageVersion Include="Wcwidth" Version="[2,3)" />
+
+		<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21,2)" />
+		<PackageVersion Include="Serilog" Version="4.2.0" />
+		<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
+		<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
+		<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
+		<PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.5,4)" />
+		<PackageVersion Include="CsvHelper" Version="[33.0.1,34)" />
+		<PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" />
+		<PackageVersion Include="System.CommandLine" Version="[2.0.0-beta4.22272.1,3)" />
+
+		<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
+
+		<PackageVersion Include="CommunityToolkit.Mvvm" Version="[8.2.2,9)" />
+		<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[8,9)" />	
+		<PackageVersion Include="ReactiveUI" Version="[20.1.1,21)" />
+		<PackageVersion Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="[1.3.1,2)" />
+		<PackageVersion Include="ReactiveUI.SourceGenerators" Version="[1.0.3,2)"/>	
+
+		<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="[17.10,18)" />
+		<PackageVersion Include="Moq" Version="[4.20.70,5)" />
+		<PackageVersion Include="ReportGenerator" Version="[5.3.8,6)" />
+		<PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[21.0.29,22)" />
+		<PackageVersion Include="xunit" Version="[2.9.0,3)" />
+		<PackageVersion Include="Xunit.Combinatorial" Version="[1.6.24,2)" />
+		<PackageVersion Include="xunit.runner.visualstudio" Version="[2.8.2,3)"/> 
+		<PackageVersion Include="coverlet.collector" Version="[6.0.2,7)" />
+		
+	</ItemGroup>
+
+	<ItemGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+        <PackageVersion Include="Terminal.Gui" Version="2.0.0" />
+	</ItemGroup>
+
+</Project>

+ 0 - 1
Example/Example.csproj

@@ -1,7 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0</TargetFramework>
     <!-- Version numbers are automatically updated by gitversion when a release is released -->
     <!-- In the source tree the version will always be 1.0 for all projects. -->
     <!-- Do not modify these. -->

+ 12 - 13
GitVersion.yml

@@ -1,10 +1,9 @@
-mode: ContinuousDeployment
+workflow: GitFlow/v1
 tag-prefix: '[vV]'
-continuous-delivery-fallback-tag: dev
 branches:
   develop:
     mode: ContinuousDeployment
-    tag: develop
+    label: develop
     regex: v2_develop
     tracks-release-branches: true
     is-source-branch-for: ['main']
@@ -12,14 +11,14 @@ branches:
 
   main:
     mode: ContinuousDeployment
-    tag: prealpha
+    label: prealpha
     regex: v2_release
     is-release-branch: true
     source-branches: ['develop']
 
   v1_develop:
     mode: ContinuousDeployment
-    tag: v1_develop
+    label: v1_develop
     regex: v1_develop
     source-branches:
     - v1_release
@@ -33,9 +32,9 @@ branches:
 
   pull-request:
     mode: ContinuousDeployment
-    tag: PullRequest.{BranchName}
+    label: PullRequest.{BranchName}
     increment: Inherit
-    tag-number-pattern: '[/-](?<number>\d+)'
+    label-number-pattern: '[/-](?<number>\d+)'
     regex: ^(pull|pull\-requests|pr)[/-]
     source-branches:
     - develop
@@ -56,13 +55,13 @@ ignore:
 # branches:
 #   # v1_develop:
 #   #   mode: ContinuousDeployment
-#   #   tag: pre
+#   #   label: pre
 #   #   regex: ^v1_develop?[/-]
 #   #   is-release-branch: false
 #   #   source-branches:
 #   #   - v1
 #   # v1:
-#   #   tag: rc
+#   #   label: rc
 #   #   increment: Patch
 #   #   regex: ^v2?[/-]
 #   #   is-release-branch: false
@@ -71,7 +70,7 @@ ignore:
 
 #   v2_develop:
 #     mode: ContinuousDeployment
-#     tag: pre
+#     label: pre
 #     regex: ^v2_develop?[/-]
 #     is-release-branch: true
 #     tracks-release-branches: true
@@ -80,13 +79,13 @@ ignore:
 #   v2:
 #     mode: ContinuousDeployment
 #     is-release-branch: false
-#     tag: alpha
+#     label: alpha
 #     increment: Patch
 #     regex: ^v2?[/-]
 #     source-branches: ['v2_develop']
 
 #   # feature:
-#   #   tag: useBranchName
+#   #   label: useBranchName
 #   #   regex: ^features?[/-]
 #   #   source-branches:
 #   #   - v1
@@ -95,7 +94,7 @@ ignore:
 #   #   - v2_develop
  
 #   pull-request:
-#     tag: PullRequest.{BranchName}
+#     label: PullRequest.{BranchName}
 #     increment: Inherit
 # ignore:
 #   sha: []

+ 1 - 3
NativeAot/NativeAot.csproj

@@ -2,8 +2,6 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0</TargetFramework>
-    <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <PublishAot>true</PublishAot>
     <InvariantGlobalization>false</InvariantGlobalization>
@@ -15,7 +13,7 @@
   </ItemGroup>
 
   <ItemGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
-    <PackageReference Include="Terminal.Gui" Version="2.0.0" />
+    <PackageReference Include="Terminal.Gui" />
     <TrimmerRootAssembly Include="Terminal.Gui" />
   </ItemGroup>
 

+ 5 - 2
NoSamples.slnf

@@ -4,7 +4,10 @@
     "projects": [
       "Terminal.Gui\\Terminal.Gui.csproj",
       "UICatalog\\UICatalog.csproj",
-      "UnitTests\\UnitTests.csproj"
-    ]
+      "Tests\\UnitTests\\UnitTests.csproj",
+      "Tests\\UnitTestsParallelizable\\UnitTests.Parallelizable.csproj",
+      "Tests\\IntegrationTests\\IntegrationTests.csproj",
+      "Tests\\StressTests\\StressTests.csproj"
+       ]
   }
 }

+ 3 - 4
ReactiveExample/ReactiveExample.csproj

@@ -1,7 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0</TargetFramework>
     <!-- Version numbers are automatically updated by gitversion when a release is released -->
     <!-- In the source tree the version will always be 2.0 for all projects. -->
     <!-- Do not modify these. -->
@@ -11,9 +10,9 @@
     <InformationalVersion>2.0</InformationalVersion>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="ReactiveUI" Version="[20.1.1,21)" />
-    <PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="[1.3.1,2)" PrivateAssets="all" />
-    <PackageReference Include="ReactiveUI.SourceGenerators" Version="[1.0.3,2)" PrivateAssets="all" />
+    <PackageReference Include="ReactiveUI" />
+    <PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" PrivateAssets="all" />
+    <PackageReference Include="ReactiveUI.SourceGenerators" PrivateAssets="all" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />

+ 4 - 1
Release.slnf

@@ -4,7 +4,10 @@
     "projects": [
       "Terminal.Gui\\Terminal.Gui.csproj",
       "UICatalog\\UICatalog.csproj",
-      "UnitTests\\UnitTests.csproj"
+      "Tests\\UnitTests\\UnitTests.csproj",
+      "Tests\\UnitTestsParallelizable\\UnitTests.Parallelizable.csproj",
+      "Tests\\IntegrationTests\\IntegrationTests.csproj",
+      "Tests\\StressTests\\StressTests.csproj"
     ]
   }
 }

二进制
Scripts/.testloop.sh.swp


+ 92 - 0
Scripts/FindDuplicateTestMethodsInSameFileName.ps1

@@ -0,0 +1,92 @@
+# FindDuplicateTestMethodsInSameFileName.ps1
+param (
+    [string]$solutionPath = ".\Tests"
+)
+
+# Set the base path for relative paths (current directory when script is run)
+$basePath = Get-Location
+
+# Define projects to ignore (add your project names or path patterns here)
+$ignoreProjects = @(
+    "StressTests"
+    # Add more as needed, e.g., "Tests/SubFolder/OldProject"
+)
+
+# Function to extract method names from a C# file
+function Get-TestMethodNames {
+    param ($filePath)
+    $content = Get-Content -Path $filePath -Raw
+    $testMethods = @()
+
+    # Match test attributes and capture method names with flexible spacing/comments
+    $methodPattern = '(?s)(\[TestMethod\]|\[Test\]|\[Fact\]|\[Theory\])\s*[\s\S]*?public\s+(?:void|Task)\s+(\w+)\s*\('
+    $methods = [regex]::Matches($content, $methodPattern)
+
+    foreach ($match in $methods) {
+        $methodName = $match.Groups[2].Value  # Group 2 is the method name
+        if ($methodName) {  # Ensure we only add non-empty method names
+            $testMethods += $methodName
+        }
+    }
+    return $testMethods
+}
+
+# Collect all test files
+$testFiles = Get-ChildItem -Path $solutionPath -Recurse -Include *.cs | 
+             Where-Object { $_.FullName -match "Tests" -or $_.FullName -match "Test" }
+
+# Group files by filename
+$fileGroups = $testFiles | Group-Object -Property Name
+
+# Dictionary to track method names and their locations, scoped to same filenames
+$duplicates = @{}
+
+foreach ($group in $fileGroups) {
+    if ($group.Count -gt 1) { # Only process files that exist in multiple locations
+        $fileName = $group.Name
+        $methodMap = @{} # Track methods for this specific filename
+
+        foreach ($file in $group.Group) {
+            # Skip files in ignored projects
+            $skipFile = $false
+            foreach ($ignore in $ignoreProjects) {
+                if ($file.FullName -like "*$ignore*") {
+                    $skipFile = $true
+                    break
+                }
+            }
+            if ($skipFile) { continue }
+
+            $methods = Get-TestMethodNames -filePath $file.FullName
+            foreach ($method in $methods) {
+                if ($methodMap.ContainsKey($method)) {
+                    # Duplicate found for this method in the same filename
+                    if (-not $duplicates.ContainsKey($method)) {
+                        $duplicates[$method] = @($methodMap[$method])
+                    }
+                    $duplicates[$method] += $file.FullName
+                } else {
+                    $methodMap[$method] = $file.FullName
+                }
+            }
+        }
+    }
+}
+
+# Output results with relative paths
+if ($duplicates.Count -eq 0) {
+    Write-Host "No duplicate test method names found in files with the same name across projects." -ForegroundColor Green
+} else {
+    Write-Host "Duplicate test method names found in files with the same name across projects:" -ForegroundColor Yellow
+    foreach ($dup in $duplicates.Keys) {
+        Write-Host "Method: $dup" -ForegroundColor Cyan
+        foreach ($fullPath in $duplicates[$dup]) {
+            $relativePath = Resolve-Path -Path $fullPath -Relative -RelativeBasePath $basePath
+            Write-Host "  - $relativePath" -ForegroundColor White
+        }
+    }
+    # Display total number of duplicate methods
+    Write-Host "Total number of duplicate methods: $($duplicates.Count)" -ForegroundColor Magenta
+    # Fail the pipeline by setting a non-zero exit code
+    exit 1
+}

+ 50 - 0
Scripts/FindDuplicateTests.ps1

@@ -0,0 +1,50 @@
+# Define the root directory containing test projects
+$testsDir = "./Tests"
+
+# Get all subfolders in the ./Tests directory
+$subfolders = Get-ChildItem -Directory $testsDir
+
+# Initialize a hashtable to track method names and their associated subfolders
+$methodMap = @{}
+
+# Iterate through each subfolder
+foreach ($subfolder in $subfolders) {
+    $subfolderName = $subfolder.Name
+    
+    # Run dotnet test --list-tests to get the list of tests in the subfolder
+    $output = dotnet test $subfolder.FullName --list-tests | Out-String
+    
+    # Split the output into lines and filter for lines containing a dot (indicative of test names)
+    $testLines = $output -split "`n" | Where-Object { $_ -match "\." }
+    
+    # Process each test line to extract the method name
+    foreach ($testLine in $testLines) {
+        $trimmed = $testLine.Trim()
+        $parts = $trimmed -split "\."
+        $lastPart = $parts[-1]
+        
+        # Handle parameterized tests by extracting the method name before any parentheses
+        if ($lastPart -match "\(") {
+            $methodName = $lastPart.Substring(0, $lastPart.IndexOf("("))
+        } else {
+            $methodName = $lastPart
+        }
+        
+        # Update the hashtable with the method name and subfolder
+        if ($methodMap.ContainsKey($methodName)) {
+            # Add the subfolder only if it’s not already listed for this method name
+            if (-not ($methodMap[$methodName] -contains $subfolderName)) {
+                $methodMap[$methodName] += $subfolderName
+            }
+        } else {
+            $methodMap[$methodName] = @($subfolderName)
+        }
+    }
+}
+
+# Identify and display duplicated test method names
+foreach ($entry in $methodMap.GetEnumerator()) {
+    if ($entry.Value.Count -gt 1) {
+        Write-Output "Duplicated test: $($entry.Key) in folders: $($entry.Value -join ', ')"
+    }
+}

+ 30 - 0
Scripts/testloop.sh

@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# This script runs the tests in a loop until they all pass.
+# It will exit if any test run fails.
+
+dotnet build -c Debug
+
+iterationCount=1
+
+while true; do
+    echo "Starting iteration $iterationCount..."
+
+    dotnet test Tests/UnitTests --no-build --diag:TestResults/UnitTests.log -- xunit.stopOnFail=true
+    if [ $? -ne 0 ]; then
+        echo "UnitTests run failed on iteration $iterationCount. Exiting."
+        exit 1
+    fi
+
+    dotnet test Tests/UnitTestsParallelizable --no-build --diag:TestResults/UnitTestsParallelizable.log -- xunit.stopOnFail=true
+    if [ $? -ne 0 ]; then
+        echo "UnitTestsParallelizable run failed on iteration $iterationCount. Exiting."
+        exit 1
+    fi
+
+    # Clean up the log files
+    rm log*
+
+    # Increment the iteration counter
+    ((iterationCount++))
+done

+ 1 - 3
SelfContained/SelfContained.csproj

@@ -2,8 +2,6 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0</TargetFramework>
-    <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <PublishTrimmed>true</PublishTrimmed>
     <TrimMode>Link</TrimMode>
@@ -18,7 +16,7 @@
   </ItemGroup>
 
   <ItemGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
-    <PackageReference Include="Terminal.Gui" Version="2.0.0" />
+    <PackageReference Include="Terminal.Gui" />
     <TrimmerRootAssembly Include="Terminal.Gui" />
   </ItemGroup>
 

+ 6 - 3
Terminal.Gui/Application/Application.Mouse.cs

@@ -62,7 +62,10 @@ public static partial class Application // Mouse handling
         }
 
 #if DEBUG_IDISPOSABLE
-        ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
+        if (View.DebugIDisposable)
+        {
+            ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
+        }
 #endif
 
         if (!RaiseUnGrabbingMouseEvent (MouseGrabView))
@@ -150,7 +153,7 @@ public static partial class Application // Mouse handling
         if (deepestViewUnderMouse is { })
         {
 #if DEBUG_IDISPOSABLE
-            if (deepestViewUnderMouse.WasDisposed)
+            if (View.DebugIDisposable && deepestViewUnderMouse.WasDisposed)
             {
                 throw new ObjectDisposedException (deepestViewUnderMouse.GetType ().FullName);
             }
@@ -278,7 +281,7 @@ public static partial class Application // Mouse handling
         if (MouseGrabView is { })
         {
 #if DEBUG_IDISPOSABLE
-            if (MouseGrabView.WasDisposed)
+            if (View.DebugIDisposable && MouseGrabView.WasDisposed)
             {
                 throw new ObjectDisposedException (MouseGrabView.GetType ().FullName);
             }

+ 35 - 27
Terminal.Gui/Application/Application.Run.cs

@@ -1,8 +1,6 @@
 #nullable enable
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-using Microsoft.CodeAnalysis.Diagnostics;
 
 namespace Terminal.Gui;
 
@@ -27,7 +25,6 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
     private static Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides
 
-
     /// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary>
     [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key ArrangeKey
@@ -97,7 +94,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
         var rs = new RunState (toplevel);
 
 #if DEBUG_IDISPOSABLE
-        if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
+        if (View.DebugIDisposable && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
         {
             // This assertion confirm if the Top was already disposed
             Debug.Assert (Top.WasDisposed);
@@ -174,7 +171,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
                 // Force leave events for any entered views in the old Top
                 if (GetLastMousePosition () is { })
                 {
-                    RaiseMouseEnterLeaveEvents (GetLastMousePosition ()!.Value, new List<View?> ());
+                    RaiseMouseEnterLeaveEvents (GetLastMousePosition ()!.Value, new ());
                 }
 
                 Top?.OnDeactivate (toplevel);
@@ -208,7 +205,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
         NotifyNewRunState?.Invoke (toplevel, new (rs));
 
         // Force an Idle event so that an Iteration (and Refresh) happen.
-        Application.Invoke (() => { });
+        Invoke (() => { });
 
         return rs;
     }
@@ -231,7 +228,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
         // If the view is not visible or enabled, don't position the cursor
         if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled)
         {
-            CursorVisibility current = CursorVisibility.Invisible;
+            var current = CursorVisibility.Invisible;
             Driver?.GetCursorVisibility (out current);
 
             if (current != CursorVisibility.Invisible)
@@ -244,7 +241,9 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
         // If the view is not visible within it's superview, don't position the cursor
         Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty });
-        Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen;
+
+        Rectangle superViewViewport =
+            mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen;
 
         if (!superViewViewport.IntersectsWith (mostFocusedViewport))
         {
@@ -305,8 +304,10 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) =>
-        ApplicationImpl.Instance.Run (errorHandler, driver);
+    public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
+    {
+        return ApplicationImpl.Instance.Run (errorHandler, driver);
+    }
 
     /// <summary>
     ///     Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
@@ -332,7 +333,10 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
     public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
-        where T : Toplevel, new() => ApplicationImpl.Instance.Run<T> (errorHandler, driver);
+        where T : Toplevel, new ()
+    {
+        return ApplicationImpl.Instance.Run<T> (errorHandler, driver);
+    }
 
     /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
     /// <remarks>
@@ -356,7 +360,8 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     ///         <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then
     ///         return control immediately.
     ///     </para>
-    ///     <para>When using <see cref="Run{T}"/> or
+    ///     <para>
+    ///         When using <see cref="Run{T}"/> or
     ///         <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.IConsoleDriver)"/>
     ///         <see cref="Init"/> will be called automatically.
     ///     </para>
@@ -372,8 +377,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     ///     RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
     ///     rethrows when null).
     /// </param>
-    public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
-        => ApplicationImpl.Instance.Run (view, errorHandler);
+    public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) { ApplicationImpl.Instance.Run (view, errorHandler); }
 
     /// <summary>Adds a timeout to the application.</summary>
     /// <remarks>
@@ -381,7 +385,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     ///     reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
     ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
     /// </remarks>
-    public static object? AddTimeout (TimeSpan time, Func<bool> callback) => ApplicationImpl.Instance.AddTimeout (time, callback);
+    public static object? AddTimeout (TimeSpan time, Func<bool> callback) { return ApplicationImpl.Instance.AddTimeout (time, callback); }
 
     /// <summary>Removes a previously scheduled timeout</summary>
     /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
@@ -393,11 +397,11 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// This method also returns
     /// <see langword="false"/>
     /// if the timeout is not found.
-    public static bool RemoveTimeout (object token) => ApplicationImpl.Instance.RemoveTimeout (token);
+    public static bool RemoveTimeout (object token) { return ApplicationImpl.Instance.RemoveTimeout (token); }
 
     /// <summary>Runs <paramref name="action"/> on the thread that is processing events</summary>
     /// <param name="action">the action to be invoked on the main processing thread.</param>
-    public static void Invoke (Action action) => ApplicationImpl.Instance.Invoke (action);
+    public static void Invoke (Action action) { ApplicationImpl.Instance.Invoke (action); }
 
     // TODO: Determine if this is really needed. The only code that calls WakeUp I can find
     // is ProgressBarStyles, and it's not clear it needs to.
@@ -406,14 +410,15 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     public static void Wakeup () { MainLoop?.Wakeup (); }
 
     /// <summary>
-    /// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out.
-    /// Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
+    ///     Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that
+    ///     need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out.
+    ///     Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
     /// </summary>
-    /// <param name="forceDraw">If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and should only be overriden for testing.</param>
-    public static void LayoutAndDraw (bool forceDraw = false)
-    {
-        ApplicationImpl.Instance.LayoutAndDraw (forceDraw);
-    }
+    /// <param name="forceDraw">
+    ///     If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and
+    ///     should only be overriden for testing.
+    /// </param>
+    public static void LayoutAndDraw (bool forceDraw = false) { ApplicationImpl.Instance.LayoutAndDraw (forceDraw); }
 
     internal static void LayoutAndDrawImpl (bool forceDraw = false)
     {
@@ -424,6 +429,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
             forceDraw = true;
             ClearScreenNextIteration = false;
         }
+
         if (forceDraw)
         {
             Driver?.ClearContents ();
@@ -431,7 +437,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
         View.SetClipToScreen ();
         View.Draw (TopLevels, neededLayout || forceDraw);
-        View.SetClipToScreen (); 
+        View.SetClipToScreen ();
         Driver?.Refresh ();
     }
 
@@ -521,11 +527,13 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// <remarks>
     ///     <para>This will cause <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to return.</para>
     ///     <para>
-    ///         Calling <see cref="RequestStop(Terminal.Gui.Toplevel)"/> is equivalent to setting the <see cref="Toplevel.Running"/>
+    ///         Calling <see cref="RequestStop(Terminal.Gui.Toplevel)"/> is equivalent to setting the
+    ///         <see cref="Toplevel.Running"/>
     ///         property on the currently running <see cref="Toplevel"/> to false.
     ///     </para>
     /// </remarks>
-    public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top);
+    public static void RequestStop (Toplevel? top = null) { ApplicationImpl.Instance.RequestStop (top); }
+
     internal static void OnNotifyStopRunState (Toplevel top)
     {
         if (EndAfterFirstIteration)

+ 3 - 3
Terminal.Gui/Application/Application.cs

@@ -63,7 +63,7 @@ public static partial class Application
             {
                 Rune rune = contents [r, c].Rune;
 
-                if (rune.DecodeSurrogatePair (out char [] sp))
+                if (rune.DecodeSurrogatePair (out char []? sp))
                 {
                     sb.Append (sp);
                 }
@@ -152,7 +152,7 @@ public static partial class Application
 #if DEBUG_IDISPOSABLE
 
         // Don't dispose the Top. It's up to caller dispose it
-        if (!ignoreDisposed && Top is { })
+        if (View.DebugIDisposable && !ignoreDisposed && Top is { })
         {
             Debug.Assert (Top.WasDisposed);
 
@@ -173,6 +173,7 @@ public static partial class Application
         MainThreadId = -1;
         Iteration = null;
         EndAfterFirstIteration = false;
+        ClearScreenNextIteration = false;
 
         // Driver stuff
         if (Driver is { })
@@ -212,7 +213,6 @@ public static partial class Application
 
         Navigation = null;
 
-        ClearScreenNextIteration = false;
 
         KeyBindings.Clear ();
         AddKeyBindings ();

+ 4 - 1
Terminal.Gui/Application/ApplicationImpl.cs

@@ -176,7 +176,10 @@ public class ApplicationImpl : IApplication
             if (runState.Toplevel is null)
             {
 #if DEBUG_IDISPOSABLE
-                Debug.Assert (Application.TopLevels.Count == 0);
+                if (View.DebugIDisposable)
+                {
+                    Debug.Assert (Application.TopLevels.Count == 0);
+                }
 #endif
                 runState.Dispose ();
 

+ 11 - 2
Terminal.Gui/Application/RunState.cs

@@ -22,7 +22,10 @@ public class RunState : IDisposable
         Dispose (true);
         GC.SuppressFinalize (this);
 #if DEBUG_IDISPOSABLE
-        WasDisposed = true;
+        if (View.DebugIDisposable)
+        {
+            WasDisposed = true;
+        }
 #endif
     }
 
@@ -52,6 +55,12 @@ public class RunState : IDisposable
     public static List<RunState> Instances = new ();
 
     /// <summary>Creates a new RunState object.</summary>
-    public RunState () { Instances.Add (this); }
+    public RunState ()
+    {
+        if (View.DebugIDisposable)
+        {
+            Instances.Add (this);
+        }
+    }
 #endif
 }

+ 3 - 0
Terminal.Gui/Configuration/ConfigurationManager.cs

@@ -326,6 +326,9 @@ public static class ConfigurationManager
     }
 
 
+    /// <summary>
+    /// Logs any Json deserialization errors that occurred during deserialization to the logging system.
+    /// </summary>
     public static void LogJsonErrors ()
     {
         if (_jsonErrors.Length > 0)

+ 1 - 1
Terminal.Gui/Configuration/ScopeJsonConverter.cs

@@ -91,7 +91,7 @@ internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccess
                         scope! [propertyName].PropertyValue =
                             JsonSerializer.Deserialize (ref reader, propertyType!, SerializerContext);
                     }
-                    catch (Exception ex)
+                    catch (Exception)
                     {
                        // Logging.Trace ($"scopeT Read: {ex}");
                     }

+ 6 - 1
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs

@@ -52,6 +52,11 @@ public class CsiKeyPattern : AnsiKeyboardParserPattern
         _pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}]|\d+~)$");
     }
 
+    /// <summary>
+    /// 
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
     protected override Key? GetKeyImpl (string input)
     {
         Match match = _pattern.Match (input);
@@ -66,7 +71,7 @@ public class CsiKeyPattern : AnsiKeyboardParserPattern
 
         Key? key = _terminators.GetValueOrDefault (terminator);
 
-        if (key != null && int.TryParse (modifierGroup, out int modifier))
+        if (key is {} && int.TryParse (modifierGroup, out int modifier))
         {
             key = modifier switch
                   {

+ 175 - 166
Terminal.Gui/Terminal.Gui.csproj

@@ -1,175 +1,184 @@
 <Project Sdk="Microsoft.NET.Sdk">
-  <!-- =================================================================== -->
-  <!-- Version numbers -->
-  <!-- Automatically updated by gitversion (run `dotnet-gitversion`)  -->
-  <!-- GitVersion.xml controls settings  -->
-  <!-- See ./README.md for more -->
-  <!-- =================================================================== -->
-  <PropertyGroup>
-    <Version>2.0.0</Version>
-  </PropertyGroup>
-  <!-- =================================================================== -->
-  <!-- Assembly name. -->
-  <!-- Referenced throughout this file for consistency. -->
-  <!-- =================================================================== -->
-<PropertyGroup>
-  <AssemblyName>Terminal.Gui</AssemblyName>
-</PropertyGroup>
+    <!-- =================================================================== -->
+    <!-- Version numbers -->
+    <!-- Automatically updated by gitversion (run `dotnet-gitversion`)  -->
+    <!-- GitVersion.xml controls settings  -->
+    <!-- See ./README.md for more -->
+    <!-- =================================================================== -->
+    <PropertyGroup>
+        <Version>2.0.0</Version>
+    </PropertyGroup>
+    <!-- =================================================================== -->
+    <!-- Assembly name. -->
+    <!-- Referenced throughout this file for consistency. -->
+    <!-- =================================================================== -->
+    <PropertyGroup>
+        <AssemblyName>Terminal.Gui</AssemblyName>
+    </PropertyGroup>
 
-  <!-- =================================================================== -->
-  <!-- .NET Build Settings -->
-  <!-- =================================================================== -->
-  <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
-    <LangVersion>12</LangVersion>
-    <RootNamespace>$(AssemblyName)</RootNamespace>
-    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-    <DefineTrace>true</DefineTrace>
-    <DebugType>portable</DebugType>
-    <DefineConstants>$(DefineConstants);CONTRACTS_FULL;CODE_ANALYSIS</DefineConstants>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <NoLogo>true</NoLogo>
-    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
-    <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
-    <IsTrimmable>true</IsTrimmable>
-    <IsAotCompatible>true</IsAotCompatible>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <DefineDebug>true</DefineDebug>
-    <DefineConstants>$(DefineConstants);DEBUG_IDISPOSABLE</DefineConstants>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
-    <Optimize>true</Optimize>
-    <VersionSuffix></VersionSuffix>
-  </PropertyGroup>
-  <!-- =================================================================== -->
-  <!-- Configuration Manager -->
-  <!-- =================================================================== -->
-  <ItemGroup>
-    <None Remove="Resources\config.json" />
-    <EmbeddedResource Include="Resources\config.json" />
-  </ItemGroup>
-  <!-- =================================================================== -->
-  <!-- Dependencies -->
-  <!-- =================================================================== -->
-  <ItemGroup>
-    <PackageReference Include="ColorHelper" Version="[1.8.1,2)" />
-    <PackageReference Include="JetBrains.Annotations" Version="[2024.2.0,)" PrivateAssets="all" />
-    <PackageReference Include="Microsoft.CodeAnalysis" Version="[4.10,5)" PrivateAssets="all" />
-    <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="[4.10,5)" PrivateAssets="all" />
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="[4.10,5)" PrivateAssets="all" />
-    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
-    <!-- Enable Nuget Source Link for github -->
-    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="[8,9)" PrivateAssets="all" />
-    <PackageReference Include="System.IO.Abstractions" Version="[21.0.22,22)" />
-    <PackageReference Include="System.Text.Json" Version="[8.0.5,9)" />
-    <PackageReference Include="Wcwidth" Version="[2,3)" />
-  </ItemGroup>
-  <!-- =================================================================== -->
-  <!-- Global Usings and Type Aliases -->
-  <!-- =================================================================== -->
-  <ItemGroup>
-    <Using Include="JetBrains.Annotations" />
-    <Using Include="JetBrains.Annotations.PureAttribute" Alias="PureAttribute" />
-    <Using Include="System.Drawing" />
-    <Using Include="System.Text" />
-    <Using Include="Terminal.Gui.EnumExtensions" />
-  </ItemGroup>
-  <!-- =================================================================== -->
-  <!-- Assembliy names for which internal items are visible -->
-  <!-- =================================================================== -->
-  <ItemGroup>
-    <InternalsVisibleTo Include="UnitTests" />
-    <InternalsVisibleTo Include="TerminalGuiDesigner" />
-    <InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
-  </ItemGroup>
-  <!-- =================================================================== -->
-  <!-- API Documentation -->
-  <!-- =================================================================== -->
-  <ItemGroup>
-    <None Include="..\docfx\images\logo.png">
-      <Pack>true</Pack>
-      <PackagePath>\</PackagePath>
-    </None>
-    <None Include="..\README.md">
-      <Pack>true</Pack>
-      <PackagePath>\</PackagePath>
-    </None>
-  </ItemGroup>
-  <!-- =================================================================== -->
-  <!-- i18 -->
-  <!-- =================================================================== -->
-  <ItemGroup>
-    <Compile Update="Resources\Strings.Designer.cs">
-      <DesignTime>true</DesignTime>
-      <AutoGen>true</AutoGen>
-      <DependentUpon>Strings.resx</DependentUpon>
-    </Compile>
-  </ItemGroup>
-  <ItemGroup>
-    <EmbeddedResource Update="Resources\Strings.resx">
-      <Generator>ResXFileCodeGenerator</Generator>
-      <LastGenOutput>Strings.Designer.cs</LastGenOutput>
-    </EmbeddedResource>
-  </ItemGroup>
-  <!-- =================================================================== -->
-  <!-- Nuget  -->
-  <!-- =================================================================== -->
-  <PropertyGroup>
-    <PackageId>$(AssemblyName)</PackageId>
-    <PackageLicenseExpression>MIT</PackageLicenseExpression>
-    <PackageProjectUrl>https://github.com/gui-cs/$(AssemblyName)</PackageProjectUrl>
-    <PackageIcon>logo.png</PackageIcon>
-    <PackageReadmeFile>README.md</PackageReadmeFile>
-    <PackageTags>csharp, terminal, c#, f#, gui, toolkit, console, tui</PackageTags>
-    <Description>Cross platform Terminal UI toolkit for .NET</Description>
-    <Owners>Miguel de Icaza, Tig Kindel</Owners>
-    <Summary>A toolkit for building rich console apps for .NET that works on Windows, Mac, and Linux/Unix.</Summary>
-    <Title>$(AssemblyName) - Cross-platform Terminal User Interface (TUI) toolkit for .NET</Title>
-    <PackageReleaseNotes>
-      See: $(PackageProjectUrl)/releases
-    </PackageReleaseNotes>
-    <DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
-    <GeneratePackageOnBuild Condition=" '$(Configuration)' == 'Debug' ">true</GeneratePackageOnBuild>
-    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
-    <RepositoryUrl>https://github.com/gui-cs/$(AssemblyName).git</RepositoryUrl>
-    <RepositoryType>git</RepositoryType>
-    <IncludeSymbols>true</IncludeSymbols>
-    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
-    <!-- Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->
-    <PublishRepositoryUrl>true</PublishRepositoryUrl>
-    <GitRepositoryRemoteName>upstream</GitRepositoryRemoteName>
-    <!-- Embed source files that are not tracked by the source control manager in the PDB -->
-    <EmbedUntrackedSources>true</EmbedUntrackedSources>
-    <EnableSourceLink>true</EnableSourceLink>
-    <Authors>Miguel de Icaza, Tig Kindel (@tig), @BDisp</Authors>
-  </PropertyGroup>
-  <ProjectExtensions><VisualStudio><UserProperties resources_4config_1json__JsonSchema="../../docfx/schemas/tui-config-schema.json" /></VisualStudio></ProjectExtensions>
+    <!-- =================================================================== -->
+    <!-- .NET Build Settings -->
+    <!-- =================================================================== -->
+    <PropertyGroup>
+        <!--Note - These three SHOULD be picked up from Directory.Build.props, but they are not. Not sure why. -->
+        <TargetFramework>net8.0</TargetFramework>
+        <LangVersion>12</LangVersion>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <!-- -->
+        <RootNamespace>$(AssemblyName)</RootNamespace>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+        <DefineTrace>true</DefineTrace>
+        <DebugType>portable</DebugType>
+        <DefineConstants>$(DefineConstants);CONTRACTS_FULL;CODE_ANALYSIS</DefineConstants>
+        <NoLogo>true</NoLogo>
+        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
+        <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
+        <IsTrimmable>true</IsTrimmable>
+        <IsAotCompatible>true</IsAotCompatible>
+    </PropertyGroup>
+    <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+        <DefineDebug>true</DefineDebug>
+        <DefineConstants>$(DefineConstants);DEBUG_IDISPOSABLE</DefineConstants>
+    </PropertyGroup>
+    <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+        <Optimize>true</Optimize>
+        <VersionSuffix></VersionSuffix>
+    </PropertyGroup>
+    <!-- =================================================================== -->
+    <!-- Configuration Manager -->
+    <!-- =================================================================== -->
+    <ItemGroup>
+        <None Remove="Resources\config.json" />
+        <EmbeddedResource Include="Resources\config.json" />
+    </ItemGroup>
+    <!-- =================================================================== -->
+    <!-- Dependencies -->
+    <!-- =================================================================== -->
+    <ItemGroup>
+        <PackageReference Include="ColorHelper" />
+        <PackageReference Include="JetBrains.Annotations" />
+        <PackageReference Include="Microsoft.CodeAnalysis" />
+        <PackageReference Include="Microsoft.CodeAnalysis.Common" />
+        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
+        <PackageReference Include="Microsoft.Extensions.Logging" />
+        <PackageReference Include="System.IO.Abstractions" />
+        <PackageReference Include="System.Text.Json" />
+        <PackageReference Include="Wcwidth" />
+        <!-- Enable Nuget Source Link for github -->
+        <PackageReference Include="Microsoft.SourceLink.GitHub" />
+    </ItemGroup>
+    <!-- =================================================================== -->
+    <!-- Global Usings and Type Aliases -->
+    <!-- =================================================================== -->
+    <ItemGroup>
+        <Using Include="JetBrains.Annotations" />
+        <Using Include="JetBrains.Annotations.PureAttribute" Alias="PureAttribute" />
+        <Using Include="System.Drawing" />
+        <Using Include="System.Text" />
+        <Using Include="Terminal.Gui.EnumExtensions" />
+    </ItemGroup>
+    <!-- =================================================================== -->
+    <!-- Assembliy names for which internal items are visible -->
+    <!-- =================================================================== -->
+    <ItemGroup>
+        <InternalsVisibleTo Include="UnitTests" />
+        <InternalsVisibleTo Include="UnitTests.Parallelizable" />
+        <InternalsVisibleTo Include="StressTests" />
+        <InternalsVisibleTo Include="IntegrationTests" />
+        <InternalsVisibleTo Include="TerminalGuiDesigner" />
+        <InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
+    </ItemGroup>
+    <!-- =================================================================== -->
+    <!-- API Documentation -->
+    <!-- =================================================================== -->
+    <ItemGroup>
+        <None Include="..\docfx\images\logo.png">
+            <Pack>true</Pack>
+            <PackagePath>\</PackagePath>
+        </None>
+        <None Include="..\README.md">
+            <Pack>true</Pack>
+            <PackagePath>\</PackagePath>
+        </None>
+    </ItemGroup>
+    <!-- =================================================================== -->
+    <!-- i18 -->
+    <!-- =================================================================== -->
+    <ItemGroup>
+        <Compile Update="Resources\Strings.Designer.cs">
+            <DesignTime>true</DesignTime>
+            <AutoGen>true</AutoGen>
+            <DependentUpon>Strings.resx</DependentUpon>
+        </Compile>
+    </ItemGroup>
+    <ItemGroup>
+        <EmbeddedResource Update="Resources\Strings.resx">
+            <Generator>ResXFileCodeGenerator</Generator>
+            <LastGenOutput>Strings.Designer.cs</LastGenOutput>
+        </EmbeddedResource>
+    </ItemGroup>
+    <!-- =================================================================== -->
+    <!-- Nuget  -->
+    <!-- =================================================================== -->
+    <PropertyGroup>
+        <PackageId>$(AssemblyName)</PackageId>
+        <PackageLicenseExpression>MIT</PackageLicenseExpression>
+        <PackageProjectUrl>https://github.com/gui-cs/$(AssemblyName)</PackageProjectUrl>
+        <PackageIcon>logo.png</PackageIcon>
+        <PackageReadmeFile>README.md</PackageReadmeFile>
+        <PackageTags>csharp, terminal, c#, f#, gui, toolkit, console, tui</PackageTags>
+        <Description>Cross platform Terminal UI toolkit for .NET</Description>
+        <Owners>Miguel de Icaza, Tig Kindel</Owners>
+        <Summary>A toolkit for building rich console apps for .NET that works on Windows, Mac, and Linux/Unix.</Summary>
+        <Title>$(AssemblyName) - Cross-platform Terminal User Interface (TUI) toolkit for .NET</Title>
+        <PackageReleaseNotes>
+            See: $(PackageProjectUrl)/releases
+        </PackageReleaseNotes>
+        <DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
+        <GeneratePackageOnBuild Condition=" '$(Configuration)' == 'Debug' ">true</GeneratePackageOnBuild>
+        <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
+        <RepositoryUrl>https://github.com/gui-cs/$(AssemblyName).git</RepositoryUrl>
+        <RepositoryType>git</RepositoryType>
+        <IncludeSymbols>true</IncludeSymbols>
+        <SymbolPackageFormat>snupkg</SymbolPackageFormat>
+        <!-- Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->
+        <PublishRepositoryUrl>true</PublishRepositoryUrl>
+        <GitRepositoryRemoteName>upstream</GitRepositoryRemoteName>
+        <!-- Embed source files that are not tracked by the source control manager in the PDB -->
+        <EmbedUntrackedSources>true</EmbedUntrackedSources>
+        <EnableSourceLink>true</EnableSourceLink>
+        <Authors>Miguel de Icaza, Tig Kindel (@tig), @BDisp</Authors>
+    </PropertyGroup>
+    <ProjectExtensions>
+        <VisualStudio>
+            <UserProperties resources_4config_1json__JsonSchema="../../docfx/schemas/tui-config-schema.json" />
+        </VisualStudio>
+    </ProjectExtensions>
 
-  <Target Name="CopyNuGetPackagesToLocalPackagesFolder" AfterTargets="Pack" Condition="'$(Configuration)' == 'Release'">
-      <PropertyGroup>
-          <!-- Define the path for local_packages relative to the project directory -->
-          <LocalPackagesPath>$(MSBuildThisFileDirectory)..\local_packages\</LocalPackagesPath>
-          <!-- Output path without framework-specific folders -->
-          <PackageOutputPath>$(MSBuildThisFileDirectory)bin\$(Configuration)\</PackageOutputPath>
-      </PropertyGroup>
+    <Target Name="CopyNuGetPackagesToLocalPackagesFolder" AfterTargets="Pack" Condition="'$(Configuration)' == 'Release'">
+        <PropertyGroup>
+            <!-- Define the path for local_packages relative to the project directory -->
+            <LocalPackagesPath>$(MSBuildThisFileDirectory)..\local_packages\</LocalPackagesPath>
+            <!-- Output path without framework-specific folders -->
+            <PackageOutputPath>$(MSBuildThisFileDirectory)bin\$(Configuration)\</PackageOutputPath>
+        </PropertyGroup>
 
-      <!-- Ensure the local_packages folder exists -->
-      <Message Text="Checking if $(LocalPackagesPath) exists, creating if necessary." Importance="high" />
-      <MakeDir Directories="$(LocalPackagesPath)" />
+        <!-- Ensure the local_packages folder exists -->
+        <Message Text="Checking if $(LocalPackagesPath) exists, creating if necessary." Importance="high" />
+        <MakeDir Directories="$(LocalPackagesPath)" />
 
-      <!-- Collect .nupkg and .snupkg files into an item group -->
-      <ItemGroup>
-          <NuGetPackages Include="$(PackageOutputPath)*.nupkg;$(PackageOutputPath)*.snupkg" />
-      </ItemGroup>
+        <!-- Collect .nupkg and .snupkg files into an item group -->
+        <ItemGroup>
+            <NuGetPackages Include="$(PackageOutputPath)*.nupkg;$(PackageOutputPath)*.snupkg" />
+        </ItemGroup>
 
-      <!-- Check if any packages were found -->
-      <Message Text="Found packages: @(NuGetPackages)" Importance="high" />
+        <!-- Check if any packages were found -->
+        <Message Text="Found packages: @(NuGetPackages)" Importance="high" />
 
-      <!-- Copy files only if found -->
-      <Copy SourceFiles="@(NuGetPackages)" DestinationFolder="$(LocalPackagesPath)" SkipUnchangedFiles="false" Condition="@(NuGetPackages) != ''" />
+        <!-- Copy files only if found -->
+        <Copy SourceFiles="@(NuGetPackages)" DestinationFolder="$(LocalPackagesPath)" SkipUnchangedFiles="false" Condition="@(NuGetPackages) != ''" />
 
-      <!-- Log success -->
-      <Message Text="Copy completed successfully." Importance="high" />
-  </Target>
+        <!-- Log success -->
+        <Message Text="Copy completed successfully." Importance="high" />
+    </Target>
 </Project>

+ 1 - 1
Terminal.Gui/View/DrawContext.cs

@@ -2,7 +2,7 @@
 namespace Terminal.Gui;
 
 /// <summary>
-///     Tracks the region that has been drawn during <see cref="View.Draw"/>. This is primarily
+///     Tracks the region that has been drawn during <see cref="View.Draw(DrawContext?)"/>. This is primarily
 ///     in support of <see cref="ViewportSettings.Transparent"/>.
 /// </summary>
 public class DrawContext

+ 2 - 2
Terminal.Gui/View/DrawEventArgs.cs

@@ -16,7 +16,7 @@ public class DrawEventArgs : CancelEventArgs
     ///     <see cref="View"/>.
     /// </param>
     /// <param name="drawContext">
-    ///     Add any regions that have been drawn to during <see cref="View.Draw"/> operations to this context. This is
+    ///     Add any regions that have been drawn to during <see cref="View.Draw(DrawContext?)"/> operations to this context. This is
     ///     primarily
     ///     in support of <see cref="ViewportSettings.Transparent"/>.
     /// </param>
@@ -34,7 +34,7 @@ public class DrawEventArgs : CancelEventArgs
     public Rectangle NewViewport { get; }
 
     /// <summary>
-    ///     Add any regions that have been drawn to during <see cref="View.Draw"/> operations to this context. This is
+    ///     Add any regions that have been drawn to during <see cref="View.Draw(DrawContext?)"/> operations to this context. This is
     ///     primarily
     ///     in support of <see cref="ViewportSettings.Transparent"/>.
     /// </summary>

+ 1 - 0
Terminal.Gui/View/View.Mouse.cs

@@ -762,6 +762,7 @@ public partial class View // Mouse APIs
     ///     INTERNAL: Gets the Views that are under the mouse at <paramref name="location"/>, including Adornments.
     /// </summary>
     /// <param name="location"></param>
+    /// <param name="ignoreTransparent"></param>
     /// <returns></returns>
     internal static List<View?> GetViewsUnderMouse (in Point location, bool ignoreTransparent = false)
     {

+ 87 - 65
Terminal.Gui/View/View.cs

@@ -24,6 +24,83 @@ namespace Terminal.Gui;
 
 public partial class View : IDisposable, ISupportInitializeNotification
 {
+    private bool _disposedValue;
+
+    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource.</summary>
+    public void Dispose ()
+    {
+        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+        Disposing?.Invoke (this, EventArgs.Empty);
+        Dispose (true);
+        GC.SuppressFinalize (this);
+#if DEBUG_IDISPOSABLE
+        if (DebugIDisposable)
+        {
+            WasDisposed = true;
+
+            foreach (View? instance in Instances.Where (
+                                                        x =>
+                                                        {
+                                                            if (x is { })
+                                                            {
+                                                                return x.WasDisposed;
+                                                            }
+
+                                                            return false;
+                                                        })
+                                                .ToList ())
+            {
+                Instances.Remove (instance);
+            }
+        }
+#endif
+    }
+
+    /// <summary>
+    ///     Riased when the <see cref="View"/> is being disposed.
+    /// </summary>
+    public event EventHandler? Disposing;
+
+    /// <summary>Pretty prints the View</summary>
+    /// <returns></returns>
+    public override string ToString () { return $"{GetType ().Name}({Id}){Frame}"; }
+
+    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+    /// <remarks>
+    ///     If disposing equals true, the method has been called directly or indirectly by a user's code. Managed and
+    ///     unmanaged resources can be disposed. If disposing equals false, the method has been called by the runtime from
+    ///     inside the finalizer and you should not reference other objects. Only unmanaged resources can be disposed.
+    /// </remarks>
+    /// <param name="disposing"></param>
+    protected virtual void Dispose (bool disposing)
+    {
+        LineCanvas.Dispose ();
+
+        DisposeMouse ();
+        DisposeKeyboard ();
+        DisposeAdornments ();
+        DisposeScrollBars ();
+
+        for (int i = InternalSubviews.Count - 1; i >= 0; i--)
+        {
+            View subview = InternalSubviews [i];
+            Remove (subview);
+            subview.Dispose ();
+        }
+
+        if (!_disposedValue)
+        {
+            if (disposing)
+            {
+                // TODO: dispose managed state (managed objects)
+            }
+
+            _disposedValue = true;
+        }
+
+        Debug.Assert (InternalSubviews.Count == 0);
+    }
+
     #region Constructors and Initialization
 
     /// <summary>Gets or sets arbitrary data for the view.</summary>
@@ -51,7 +128,10 @@ public partial class View : IDisposable, ISupportInitializeNotification
     public View ()
     {
 #if DEBUG_IDISPOSABLE
-        Instances.Add (this);
+        if (DebugIDisposable)
+        {
+            Instances.Add (this);
+        }
 #endif
 
         SetupAdornments ();
@@ -168,6 +248,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
             // TODO: Figure out how to move this out of here and just depend on LayoutNeeded in Mainloop
             Layout (); // the EventLog in AllViewsTester fails to layout correctly if this is not here (convoluted Dim.Fill(Func)).
         }
+
         SetNeedsLayout ();
 
         Initialized?.Invoke (this, EventArgs.Empty);
@@ -371,7 +452,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
         get
         {
 #if DEBUG_IDISPOSABLE
-            if (WasDisposed)
+            if (DebugIDisposable && WasDisposed)
             {
                 throw new ObjectDisposedException (GetType ().FullName);
             }
@@ -381,7 +462,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
         set
         {
 #if DEBUG_IDISPOSABLE
-            if (WasDisposed)
+            if (DebugIDisposable && WasDisposed)
             {
                 throw new ObjectDisposedException (GetType ().FullName);
             }
@@ -450,71 +531,12 @@ public partial class View : IDisposable, ISupportInitializeNotification
 
     #endregion
 
-    /// <summary>Pretty prints the View</summary>
-    /// <returns></returns>
-    public override string ToString () { return $"{GetType ().Name}({Id}){Frame}"; }
-
-    private bool _disposedValue;
-
-    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
-    /// <remarks>
-    ///     If disposing equals true, the method has been called directly or indirectly by a user's code. Managed and
-    ///     unmanaged resources can be disposed. If disposing equals false, the method has been called by the runtime from
-    ///     inside the finalizer and you should not reference other objects. Only unmanaged resources can be disposed.
-    /// </remarks>
-    /// <param name="disposing"></param>
-    protected virtual void Dispose (bool disposing)
-    {
-        LineCanvas.Dispose ();
-
-        DisposeMouse ();
-        DisposeKeyboard ();
-        DisposeAdornments ();
-        DisposeScrollBars ();
-
-        for (int i = InternalSubviews.Count - 1; i >= 0; i--)
-        {
-            View subview = InternalSubviews [i];
-            Remove (subview);
-            subview.Dispose ();
-        }
-
-        if (!_disposedValue)
-        {
-            if (disposing)
-            {
-                // TODO: dispose managed state (managed objects)
-            }
-
-            _disposedValue = true;
-        }
-
-        Debug.Assert (InternalSubviews.Count == 0);
-    }
-
+#if DEBUG_IDISPOSABLE
     /// <summary>
-    ///     Riased when the <see cref="View"/> is being disposed.
+    ///     Set to false to disable the debug IDisposable feature.
     /// </summary>
-    public event EventHandler? Disposing;
+    public static bool DebugIDisposable { get; set; } = false;
 
-    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource.</summary>
-    public void Dispose ()
-    {
-        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
-        Disposing?.Invoke (this, EventArgs.Empty);
-        Dispose (true);
-        GC.SuppressFinalize (this);
-#if DEBUG_IDISPOSABLE
-        WasDisposed = true;
-
-        foreach (View instance in Instances.Where (x => x.WasDisposed).ToList ())
-        {
-            Instances.Remove (instance);
-        }
-#endif
-    }
-
-#if DEBUG_IDISPOSABLE
     /// <summary>For debug purposes to verify objects are being disposed properly</summary>
     public bool WasDisposed { get; set; }
 

+ 2 - 0
Terminal.Gui/Views/Bar.cs

@@ -109,11 +109,13 @@ public class Bar : View, IOrientation, IDesignable
         set => _orientationHelper.Orientation = value;
     }
 
+#pragma warning disable CS0067 // The event is never used
     /// <inheritdoc/>
     public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
 
     /// <inheritdoc/>
     public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
+#pragma warning restore CS0067 // The event is never used
 
     /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
     /// <param name="newOrientation"></param>

+ 2 - 2
Terminal.Gui/Views/Dialog.cs

@@ -107,7 +107,7 @@ public class Dialog : Window
         get
         {
 #if DEBUG_IDISPOSABLE
-            if (WasDisposed)
+            if (View.DebugIDisposable && WasDisposed)
             {
                 throw new ObjectDisposedException (GetType ().FullName);
             }
@@ -117,7 +117,7 @@ public class Dialog : Window
         set
         {
 #if DEBUG_IDISPOSABLE
-            if (WasDisposed)
+            if (View.DebugIDisposable && WasDisposed)
             {
                 throw new ObjectDisposedException (GetType ().FullName);
             }

+ 2 - 0
Terminal.Gui/Views/Line.cs

@@ -33,11 +33,13 @@ public class Line : View, IOrientation
         set => _orientationHelper.Orientation = value;
     }
 
+#pragma warning disable CS0067 // The event is never used
     /// <inheritdoc/>
     public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
 
     /// <inheritdoc/>
     public event EventHandler<EventArgs<Orientation>> OrientationChanged;
+#pragma warning restore CS0067 // The event is never used
 
     /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
     /// <param name="newOrientation"></param>

+ 2 - 0
Terminal.Gui/Views/RadioGroup.cs

@@ -403,11 +403,13 @@ public class RadioGroup : View, IDesignable, IOrientation
 
     private readonly OrientationHelper _orientationHelper;
 
+#pragma warning disable CS0067 // The event is never used
     /// <inheritdoc/>
     public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
 
     /// <inheritdoc/>
     public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
+#pragma warning restore CS0067 // The event is never used
 
     /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
     /// <param name="newOrientation"></param>

+ 2 - 0
Terminal.Gui/Views/ScrollBar/ScrollBar.cs

@@ -164,11 +164,13 @@ public class ScrollBar : View, IOrientation, IDesignable
         set => _orientationHelper.Orientation = value;
     }
 
+#pragma warning disable CS0067 // The event is never used
     /// <inheritdoc/>
     public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
 
     /// <inheritdoc/>
     public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
+#pragma warning restore CS0067 // The event is never used
 
     /// <inheritdoc/>
     public void OnOrientationChanged (Orientation newOrientation)

+ 1 - 1
Terminal.Gui/Views/Wizard/WizardStep.cs

@@ -125,7 +125,7 @@ public class WizardStep : View
     {
         _contentView.Add (view);
 
-        if (view.CanFocus)
+        if (view!.CanFocus)
         {
             CanFocus = true;
         }

+ 31 - 7
Terminal.sln

@@ -6,8 +6,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Gui", "Terminal.Gu
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UICatalog", "UICatalog\UICatalog.csproj", "{88979F89-9A42-448F-AE3E-3060145F6375}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\UnitTests.csproj", "{8B901EDE-8974-4820-B100-5226917E2990}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveExample", "ReactiveExample\ReactiveExample.csproj", "{44E15B48-0DB2-4560-82BD-D3B7989811C3}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example", "Example\Example.csproj", "{B0A602CD-E176-449D-8663-64238D54F857}"
@@ -21,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Settings", "Settings", "{B8
 		.editorconfig = .editorconfig
 		.filenesting.json = .filenesting.json
 		.gitignore = .gitignore
+		Directory.Build.props = Directory.Build.props
+		Directory.Packages.props = Directory.Packages.props
 		GitVersion.yml = GitVersion.yml
 		global.json = global.json
 		nuget.config = nuget.config
@@ -31,9 +31,13 @@ EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{13BB2C46-B324-4B9C-92EB-CE6184D4736E}"
 	ProjectSection(SolutionItems) = preProject
 		.github\workflows\api-docs.yml = .github\workflows\api-docs.yml
-		.github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml
+		.github\workflows\build-release.yml = .github\workflows\build-release.yml
+		.github\workflows\check-duplicates.yml = .github\workflows\check-duplicates.yml
 		GitVersion.yml = GitVersion.yml
+		.github\workflows\integration-tests.yml = .github\workflows\integration-tests.yml
 		.github\workflows\publish.yml = .github\workflows\publish.yml
+		.github\workflows\stress-tests.yml = .github\workflows\stress-tests.yml
+		.github\workflows\unit-tests.yml = .github\workflows\unit-tests.yml
 	EndProjectSection
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{C7A51224-5E0F-4986-AB37-A6BF89966C12}"
@@ -50,6 +54,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAot", "NativeAot\Nati
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "Tests\UnitTests\UnitTests.csproj", "{038B09F5-EF3A-F21E-7C10-A6551866ECE2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "Tests\IntegrationTests\IntegrationTests.csproj", "{F74EC349-B988-FCFA-A1E5-967F70FB75B5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "Tests\StressTests\StressTests.csproj", "{96ACE8BA-2E07-7537-FBF2-E8176CCB8080}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -64,10 +76,6 @@ Global
 		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.Build.0 = Release|Any CPU
-		{8B901EDE-8974-4820-B100-5226917E2990}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{8B901EDE-8974-4820-B100-5226917E2990}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{8B901EDE-8974-4820-B100-5226917E2990}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{8B901EDE-8974-4820-B100-5226917E2990}.Release|Any CPU.Build.0 = Release|Any CPU
 		{44E15B48-0DB2-4560-82BD-D3B7989811C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{44E15B48-0DB2-4560-82BD-D3B7989811C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{44E15B48-0DB2-4560-82BD-D3B7989811C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -92,6 +100,22 @@ Global
 		{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}.Release|Any CPU.Build.0 = Release|Any CPU
+		{038B09F5-EF3A-F21E-7C10-A6551866ECE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{038B09F5-EF3A-F21E-7C10-A6551866ECE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{038B09F5-EF3A-F21E-7C10-A6551866ECE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{038B09F5-EF3A-F21E-7C10-A6551866ECE2}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F74EC349-B988-FCFA-A1E5-967F70FB75B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F74EC349-B988-FCFA-A1E5-967F70FB75B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F74EC349-B988-FCFA-A1E5-967F70FB75B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F74EC349-B988-FCFA-A1E5-967F70FB75B5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{96ACE8BA-2E07-7537-FBF2-E8176CCB8080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{96ACE8BA-2E07-7537-FBF2-E8176CCB8080}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{96ACE8BA-2E07-7537-FBF2-E8176CCB8080}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{96ACE8BA-2E07-7537-FBF2-E8176CCB8080}.Release|Any CPU.Build.0 = Release|Any CPU
+		{DE780834-190A-8277-51FD-750CC666E82D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{DE780834-190A-8277-51FD-750CC666E82D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 2 - 0
Terminal.sln.DotSettings

@@ -409,10 +409,12 @@
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002EMemberReordering_002EMigrations_002ECSharpFileLayoutPatternRemoveIsAttributeUpgrade/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
+	<s:Int64 x:Key="/Default/Environment/UnitTesting/ParallelProcessesCount/@EntryValue">5</s:Int64>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Justifier/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=langword/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Roslynator/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevel/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevels/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Ungrab/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=unsynchronized/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=BUGBUG/@EntryIndexedValue">True</s:Boolean>

+ 40 - 0
Tests/IntegrationTests/IntegrationTests.csproj

@@ -0,0 +1,40 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <Nullable>enable</Nullable>
+        <IsPackable>false</IsPackable>
+        <IsTestProject>true</IsTestProject>
+        <DefineConstants>$(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL</DefineConstants>
+        <DebugType>portable</DebugType>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <NoLogo>true</NoLogo>
+        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Configuration)'=='Debug'">
+        <DefineDebug>true</DefineDebug>
+        <DefineConstants>$(DefineConstants);DEBUG_IDISPOSABLE</DefineConstants>
+    </PropertyGroup>
+    <PropertyGroup Condition="'$(Configuration)'=='Release'">
+        <Optimize>true</Optimize>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="coverlet.collector" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" />
+        <PackageReference Include="xunit" />
+        <PackageReference Include="xunit.runner.visualstudio" />
+    </ItemGroup>
+    <ItemGroup>
+        <ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
+        <ProjectReference Include="..\..\UICatalog\UICatalog.csproj" />
+        <ProjectReference Include="..\UnitTests\UnitTests.csproj" />
+    </ItemGroup>
+    <ItemGroup>
+        <Using Include="Xunit" />
+    </ItemGroup>
+    <ItemGroup>
+        <None Update="xunit.runner.json">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+    </ItemGroup>
+</Project>

+ 7 - 165
UnitTests/UICatalog/ScenarioTests.cs → Tests/IntegrationTests/UICatalog/ScenarioTests.cs

@@ -1,15 +1,19 @@
 using System.Collections.ObjectModel;
 using System.Diagnostics;
 using System.Reflection;
+using Terminal.Gui;
+using UnitTests;
+using UICatalog;
 using Xunit.Abstractions;
 
-namespace UICatalog.Tests;
+namespace IntegrationTests.UICatalog;
 
 public class ScenarioTests : TestsAllViews
 {
     public ScenarioTests (ITestOutputHelper output)
     {
 #if DEBUG_IDISPOSABLE
+        View.DebugIDisposable = true;
         View.Instances.Clear ();
 #endif
         _output = output;
@@ -137,169 +141,7 @@ public class ScenarioTests : TestsAllViews
         }
     }
 
-    /// <summary>
-    ///     <para>This runs through all Scenarios defined in UI Catalog, calling Init, Setup, and Run and measuring the perf of each.</para>
-    /// </summary>
-    [Theory]
-    [MemberData (nameof (AllScenarioTypes))]
-    public void All_Scenarios_Benchmark (Type scenarioType)
-    {
-        Assert.Null (_timeoutLock);
-        _timeoutLock = new ();
-
-        // Disable any UIConfig settings
-        ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
-        ConfigurationManager.Locations = ConfigLocations.Default;
-
-        // If a previous test failed, this will ensure that the Application is in a clean state
-        Application.ResetState (true);
-
-        uint maxIterations = 1000;
-        uint abortTime = 2000;
-        object timeout = null;
-        var initialized = false;
-        var shutdown = false;
-
-        int iterationCount = 0;
-        int clearedContentCount = 0;
-        int refreshedCount = 0;
-        int updatedCount = 0;
-        int drawCompleteCount = 0;
-
-        int addedCount = 0;
-        int laidOutCount = 0;
-
-        _output.WriteLine ($"Running Scenario '{scenarioType}'");
-        var scenario = (Scenario)Activator.CreateInstance (scenarioType);
-
-        Stopwatch stopwatch = null;
-
-        Application.InitializedChanged += OnApplicationOnInitializedChanged;
-        Application.ForceDriver = "FakeDriver";
-        scenario!.Main ();
-        scenario.Dispose ();
-        scenario = null;
-        Application.ForceDriver = string.Empty;
-        Application.InitializedChanged -= OnApplicationOnInitializedChanged;
-
-        lock (_timeoutLock)
-        {
-            if (timeout is { })
-            {
-                timeout = null;
-            }
-        }
-
-        lock (_timeoutLock)
-        {
-            _timeoutLock = null;
-        }
-
-        _output.WriteLine ($"Scenario {scenarioType}");
-        _output.WriteLine ($"  took {stopwatch.ElapsedMilliseconds} ms to run.");
-        _output.WriteLine ($"  called Driver.ClearContents {clearedContentCount} times.");
-        _output.WriteLine ($"  called Driver.Refresh {refreshedCount} times.");
-        _output.WriteLine ($"    which updated the screen {updatedCount} times.");
-        _output.WriteLine ($"  called View.Draw {drawCompleteCount} times.");
-        _output.WriteLine ($"  added {addedCount} views.");
-        _output.WriteLine ($"  called View.LayoutComplete {laidOutCount} times.");
-
-        // Restore the configuration locations
-        ConfigurationManager.Locations = savedConfigLocations;
-        ConfigurationManager.Reset ();
-
-        return;
-
-        void OnApplicationOnInitializedChanged (object s, EventArgs<bool> a)
-        {
-            if (a.CurrentValue)
-            {
-                lock (_timeoutLock)
-                {
-                    timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback);
-                }
-
-                initialized = true;
-                Application.Iteration += OnApplicationOnIteration;
-                Application.Driver!.ClearedContents += (sender, args) => clearedContentCount++;
-
-                if (Application.Driver is ConsoleDriver cd)
-                {
-                    cd!.Refreshed += (sender, args) =>
-                                     {
-                                         refreshedCount++;
-
-                                         if (args.CurrentValue)
-                                         {
-                                             updatedCount++;
-                                         }
-                                     };
-                }
-
-                Application.NotifyNewRunState += OnApplicationNotifyNewRunState;
-
-                stopwatch = Stopwatch.StartNew ();
-            }
-            else
-            {
-                shutdown = true;
-                Application.NotifyNewRunState -= OnApplicationNotifyNewRunState;
-                Application.Iteration -= OnApplicationOnIteration;
-                stopwatch!.Stop ();
-            }
-            _output.WriteLine ($"Initialized == {a.CurrentValue}");
-        }
-
-        void OnApplicationOnIteration (object s, IterationEventArgs a)
-        {
-            iterationCount++;
-            if (iterationCount > maxIterations)
-            {
-                // Press QuitKey
-                _output.WriteLine ($"Attempting to quit scenario with RequestStop");
-                Application.RequestStop ();
-            }
-        }
-
-
-        void OnApplicationNotifyNewRunState (object sender, RunStateEventArgs e)
-        {
-            // Get a list of all subviews under Application.Top (and their subviews, etc.)
-            // and subscribe to their DrawComplete event
-            void SubscribeAllSubviews (View view)
-            {
-                view.DrawComplete += (s, a) => drawCompleteCount++;
-                view.SubviewsLaidOut += (s, a) => laidOutCount++;
-                view.Added += (s, a) => addedCount++;
-                foreach (View subview in view.Subviews)
-                {
-                    SubscribeAllSubviews (subview);
-                }
-            }
-
-            SubscribeAllSubviews (Application.Top);
-        }
-
-        // If the scenario doesn't close within the abort time, this will force it to quit
-        bool ForceCloseCallback ()
-        {
-            lock (_timeoutLock)
-            {
-                if (timeout is { })
-                {
-                    timeout = null;
-                }
-            }
-
-            _output.WriteLine(
-                         $"'{scenario.GetName ()}' failed to Quit with {Application.QuitKey} after {abortTime}ms and {iterationCount} iterations. Force quit.");
-
-            Application.RequestStop ();
-
-            return false;
-        }
-    }
-
+    
     public static IEnumerable<object []> AllScenarioTypes =>
         typeof (Scenario).Assembly
                          .GetTypes ()
@@ -344,7 +186,7 @@ public class ScenarioTests : TestsAllViews
 
         var top = new Toplevel ();
 
-        _viewClasses = TestHelpers.GetAllViewClasses ().ToDictionary (t => t.Name);
+        _viewClasses = ViewTestHelpers.GetAllViewClasses ().ToDictionary (t => t.Name);
 
         _leftPane = new ()
         {

+ 0 - 0
UnitTests/xunit.runner.json → Tests/IntegrationTests/xunit.runner.json


+ 29 - 0
Tests/README.md

@@ -0,0 +1,29 @@
+# Terminal.Gui Tests
+
+This folder contains the tests for Terminal.Gui.
+
+## ./UnitTests
+
+This folder contains the unit tests for Terminal.Gui that can not be run in parallel. This is because they
+depend on `Application` or other class that use static state or `ConfigurationManager`.
+
+We should be striving to move as many tests as possible to the `./UnitTestsParallelizable` folder.
+
+## ./UnitTestsParallelizable
+
+This folder contains the unit tests for Terminal.Gui that can be run in parallel.
+
+## ./IntegrationTests
+
+This folder contains the integration tests for Terminal.Gui.
+
+## ./StressTests
+
+This folder contains the stress tests for Terminal.Gui.
+
+## ./PerformanceTests
+
+This folder WILL contain the performance tests for Terminal.Gui.
+
+
+See the [Testing wiki](https://github.com/gui-cs/Terminal.Gui/wiki/Testing) for details on how to add more tests.

+ 111 - 0
Tests/StressTests/ApplicationStressTests.cs

@@ -0,0 +1,111 @@
+using Terminal.Gui;
+using UnitTests;
+using Xunit.Abstractions;
+
+namespace StressTests;
+
+public class ApplicationStressTests : TestsAllViews
+{
+    public ApplicationStressTests (ITestOutputHelper output)
+    {
+        ConsoleDriver.RunningUnitTests = true;
+        ConfigurationManager.Locations = ConfigLocations.Default;
+    }
+
+    private static volatile int _tbCounter;
+    private static readonly ManualResetEventSlim _wakeUp = new (false);
+
+    [Theory]
+    [InlineData (typeof (FakeDriver))]
+    [InlineData (typeof (NetDriver), Skip = "System.IO.IOException: The handle is invalid")]
+    //[InlineData (typeof (ANSIDriver))]
+    [InlineData (typeof (WindowsDriver))]
+    [InlineData (typeof (CursesDriver), Skip = "Unable to load DLL 'libc' or one of its dependencies: The specified module could not be found. (0x8007007E)")]
+    public async Task InvokeLeakTest (Type driverType)
+    {
+
+        Application.Init (driverName: driverType.Name);
+        Random r = new ();
+        TextField tf = new ();
+        var top = new Toplevel ();
+        top.Add (tf);
+
+        const int NUM_PASSES = 50;
+        const int NUM_INCREMENTS = 500;
+        const int POLL_MS = 100;
+        _tbCounter = 0;
+
+        Task task = Task.Run (() => RunTest (r, tf, NUM_PASSES, NUM_INCREMENTS, POLL_MS));
+
+        // blocks here until the RequestStop is processed at the end of the test
+        Application.Run (top);
+
+        await task; // Propagate exception if any occurred
+
+        Assert.Equal (NUM_INCREMENTS * NUM_PASSES, _tbCounter);
+        top.Dispose ();
+        Application.Shutdown ();
+
+        return;
+
+        static void RunTest (Random r, TextField tf, int numPasses, int numIncrements, int pollMs)
+        {
+            for (var j = 0; j < numPasses; j++)
+            {
+                _wakeUp.Reset ();
+
+                for (var i = 0; i < numIncrements; i++)
+                {
+                    Launch (r, tf, (j + 1) * numIncrements);
+                }
+
+                while (_tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
+                {
+                    int tbNow = _tbCounter;
+                    _wakeUp.Wait (pollMs);
+
+                    if (_tbCounter != tbNow)
+                    {
+                        continue;
+                    }
+
+                    // No change after wait: Idle handlers added via Application.Invoke have gone missing
+                    Application.Invoke (() => Application.RequestStop ());
+
+                    throw new TimeoutException (
+                                                $"Timeout: Increment lost. _tbCounter ({_tbCounter}) didn't "
+                                                + $"change after waiting {pollMs} ms. Failed to reach {(j + 1) * numIncrements} on pass {j + 1}"
+                                               );
+                }
+
+                ;
+            }
+
+            Application.Invoke (() => Application.RequestStop ());
+        }
+
+        static void Launch (Random r, TextField tf, int target)
+        {
+            Task.Run (
+                      () =>
+                      {
+                          Thread.Sleep (r.Next (2, 4));
+
+                          Application.Invoke (
+                                              () =>
+                                              {
+                                                  tf.Text = $"index{r.Next ()}";
+                                                  Interlocked.Increment (ref _tbCounter);
+
+                                                  if (target == _tbCounter)
+                                                  {
+                                                      // On last increment wake up the check
+                                                      _wakeUp.Set ();
+                                                  }
+                                              }
+                                             );
+                      }
+                     );
+        }
+    }
+}

+ 193 - 0
Tests/StressTests/ScenariosStressTests.cs

@@ -0,0 +1,193 @@
+using System.Diagnostics;
+using Terminal.Gui;
+using UICatalog;
+using UnitTests;
+using Xunit.Abstractions;
+
+namespace StressTests;
+
+public class ScenariosStressTests : TestsAllViews
+{
+    public ScenariosStressTests (ITestOutputHelper output)
+    {
+#if DEBUG_IDISPOSABLE
+        View.DebugIDisposable = true;
+        View.Instances.Clear ();
+#endif
+        _output = output;
+    }
+
+    private readonly ITestOutputHelper _output;
+
+    private object? _timeoutLock;
+
+    /// <summary>
+    ///     <para>
+    ///         This runs through all Scenarios defined in UI Catalog, calling Init, Setup, and Run and measuring the perf of
+    ///         each.
+    ///     </para>
+    /// </summary>
+    [Theory]
+    [MemberData (nameof (AllScenarioTypes))]
+    public void All_Scenarios_Benchmark (Type scenarioType)
+    {
+        Assert.Null (_timeoutLock);
+        _timeoutLock = new ();
+
+        // Disable any UIConfig settings
+        ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
+        ConfigurationManager.Locations = ConfigLocations.Default;
+
+        // If a previous test failed, this will ensure that the Application is in a clean state
+        Application.ResetState (true);
+
+        uint maxIterations = 1000;
+        uint abortTime = 2000;
+        object? timeout = null;
+
+        var iterationCount = 0;
+        var clearedContentCount = 0;
+        var refreshedCount = 0;
+        var updatedCount = 0;
+        var drawCompleteCount = 0;
+
+        var addedCount = 0;
+        var laidOutCount = 0;
+
+        _output.WriteLine ($"Running Scenario '{scenarioType}'");
+        var scenario = (Scenario)Activator.CreateInstance (scenarioType)!;
+
+        Stopwatch? stopwatch = null;
+
+        Application.InitializedChanged += OnApplicationOnInitializedChanged;
+        Application.ForceDriver = "FakeDriver";
+        scenario!.Main ();
+        scenario.Dispose ();
+        scenario = null;
+        Application.ForceDriver = string.Empty;
+        Application.InitializedChanged -= OnApplicationOnInitializedChanged;
+
+        lock (_timeoutLock)
+        {
+            if (timeout is { })
+            {
+                timeout = null;
+            }
+        }
+
+        lock (_timeoutLock)
+        {
+            _timeoutLock = null;
+        }
+
+        _output.WriteLine ($"Scenario {scenarioType}");
+        _output.WriteLine ($"  took {stopwatch!.ElapsedMilliseconds} ms to run.");
+        _output.WriteLine ($"  called Driver.ClearContents {clearedContentCount} times.");
+        _output.WriteLine ($"  called Driver.Refresh {refreshedCount} times.");
+        _output.WriteLine ($"    which updated the screen {updatedCount} times.");
+        _output.WriteLine ($"  called View.Draw {drawCompleteCount} times.");
+        _output.WriteLine ($"  added {addedCount} views.");
+        _output.WriteLine ($"  called View.LayoutComplete {laidOutCount} times.");
+
+        // Restore the configuration locations
+        ConfigurationManager.Locations = savedConfigLocations;
+        ConfigurationManager.Reset ();
+
+        return;
+
+        void OnApplicationOnInitializedChanged (object? s, EventArgs<bool> a)
+        {
+            if (a.CurrentValue)
+            {
+                lock (_timeoutLock)
+                {
+                    timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback);
+                }
+
+                Application.Iteration += OnApplicationOnIteration;
+                Application.Driver!.ClearedContents += (sender, args) => clearedContentCount++;
+
+                if (Application.Driver is ConsoleDriver cd)
+                {
+                    cd!.Refreshed += (sender, args) =>
+                                     {
+                                         refreshedCount++;
+
+                                         if (args.CurrentValue)
+                                         {
+                                             updatedCount++;
+                                         }
+                                     };
+                }
+
+                Application.NotifyNewRunState += OnApplicationNotifyNewRunState;
+
+                stopwatch = Stopwatch.StartNew ();
+            }
+            else
+            {
+                Application.NotifyNewRunState -= OnApplicationNotifyNewRunState;
+                Application.Iteration -= OnApplicationOnIteration;
+                stopwatch!.Stop ();
+            }
+
+            _output.WriteLine ($"Initialized == {a.CurrentValue}");
+        }
+
+        void OnApplicationOnIteration (object? s, IterationEventArgs a)
+        {
+            iterationCount++;
+
+            if (iterationCount > maxIterations)
+            {
+                // Press QuitKey
+                _output.WriteLine ("Attempting to quit scenario with RequestStop");
+                Application.RequestStop ();
+            }
+        }
+
+        void OnApplicationNotifyNewRunState (object? sender, RunStateEventArgs e)
+        {
+            // Get a list of all subviews under Application.Top (and their subviews, etc.)
+            // and subscribe to their DrawComplete event
+            void SubscribeAllSubviews (View view)
+            {
+                view.DrawComplete += (s, a) => drawCompleteCount++;
+                view.SubviewsLaidOut += (s, a) => laidOutCount++;
+                view.Added += (s, a) => addedCount++;
+
+                foreach (View subview in view.Subviews)
+                {
+                    SubscribeAllSubviews (subview);
+                }
+            }
+
+            SubscribeAllSubviews (Application.Top!);
+        }
+
+        // If the scenario doesn't close within the abort time, this will force it to quit
+        bool ForceCloseCallback ()
+        {
+            lock (_timeoutLock)
+            {
+                if (timeout is { })
+                {
+                    timeout = null;
+                }
+            }
+
+            _output.WriteLine (
+                               $"'{scenario!.GetName ()}' failed to Quit with {Application.QuitKey} after {abortTime}ms and {iterationCount} iterations. Force quit.");
+
+            Application.RequestStop ();
+
+            return false;
+        }
+    }
+
+    public static IEnumerable<object []> AllScenarioTypes =>
+        typeof (Scenario).Assembly
+                         .GetTypes ()
+                         .Where (type => type.IsClass && !type.IsAbstract && type.IsSubclassOf (typeof (Scenario)))
+                         .Select (type => new object [] { type });
+}

+ 44 - 0
Tests/StressTests/StressTests.csproj

@@ -0,0 +1,44 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <Nullable>enable</Nullable>
+
+        <IsPackable>false</IsPackable>
+        <IsTestProject>true</IsTestProject>
+	    <DefineConstants>$(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL</DefineConstants>
+	    <DebugType>portable</DebugType>
+	    <ImplicitUsings>enable</ImplicitUsings>
+	    <NoLogo>true</NoLogo>
+	    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
+        
+    </PropertyGroup>
+	<PropertyGroup Condition="'$(Configuration)'=='Debug'">
+		<DefineDebug>true</DefineDebug>
+		<DefineConstants>$(DefineConstants);DEBUG_IDISPOSABLE</DefineConstants>
+	</PropertyGroup>
+	<PropertyGroup Condition="'$(Configuration)'=='Release'">
+		<Optimize>true</Optimize>
+	</PropertyGroup>
+	<ItemGroup>
+		<Compile Include="..\UnitTests\TestsAllViews.cs" Link="TestsAllViews.cs" />
+	</ItemGroup>
+    <ItemGroup>
+        <PackageReference Include="coverlet.collector" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" />
+        <PackageReference Include="xunit" />
+        <PackageReference Include="xunit.runner.visualstudio" />
+    </ItemGroup>
+    <ItemGroup>
+        <ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
+        <ProjectReference Include="..\..\UICatalog\UICatalog.csproj" />
+    </ItemGroup>
+    <ItemGroup>
+        <Using Include="Xunit" />
+    </ItemGroup>
+    <ItemGroup>
+      <None Update="xunit.runner.json">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </None>
+    </ItemGroup>
+
+</Project>

+ 6 - 0
Tests/StressTests/xunit.runner.json

@@ -0,0 +1,6 @@
+{
+  "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
+  "parallelizeTestCollections": false,
+  "parallelizeAssembly": false,
+  "stopOnFail": false
+}

+ 0 - 0
UnitTests/Application/Application.NavigationTests.cs → Tests/UnitTests/Application/Application.NavigationTests.cs


+ 16 - 5
UnitTests/Application/ApplicationScreenTests.cs → Tests/UnitTests/Application/ApplicationScreenTests.cs

@@ -2,8 +2,14 @@
 
 namespace Terminal.Gui.ApplicationTests;
 
-public class ApplicationScreenTests (ITestOutputHelper output)
+public class ApplicationScreenTests
 {
+    public ApplicationScreenTests (ITestOutputHelper output)
+    {
+        ConsoleDriver.RunningUnitTests = true;
+    }
+
+
     [Fact]
     public void ClearScreenNextIteration_Resets_To_False_After_LayoutAndDraw ()
     {
@@ -27,12 +33,13 @@ public class ApplicationScreenTests (ITestOutputHelper output)
     {
         // Arrange
         Application.Init (new FakeDriver ());
-        Application.Top = new Toplevel ();
+        Application.Top = new ();
         Application.TopLevels.Push (Application.Top);
 
-        int clearedContentsRaised = 0;
+        var clearedContentsRaised = 0;
+
 
-        Application.Driver!.ClearedContents += (e, a) => clearedContentsRaised++;
+        Application.Driver!.ClearedContents += OnClearedContents;
 
         // Act
         Application.LayoutAndDraw ();
@@ -64,9 +71,13 @@ public class ApplicationScreenTests (ITestOutputHelper output)
         // Cleanup
         Application.Top.Dispose ();
         Application.Top = null;
+        Application.Driver!.ClearedContents -= OnClearedContents;
         Application.Shutdown ();
         Application.ResetState (true);
 
+        return;
+
+        void OnClearedContents (object e, EventArgs a) { clearedContentsRaised++; }
     }
 
     [Fact]
@@ -80,7 +91,7 @@ public class ApplicationScreenTests (ITestOutputHelper output)
         Assert.Equal (new (0, 0, 25, 25), Application.Screen);
 
         // Act
-        (((FakeDriver)Application.Driver)!).SetBufferSize (120, 30);
+        ((FakeDriver)Application.Driver)!.SetBufferSize (120, 30);
 
         // Assert
         Assert.Equal (new (0, 0, 120, 30), Application.Screen);

+ 2 - 47
UnitTests/Application/ApplicationTests.cs → Tests/UnitTests/Application/ApplicationTests.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using UnitTests;
 using Xunit.Abstractions;
 using static Terminal.Gui.ConfigurationManager;
 
@@ -15,6 +16,7 @@ public class ApplicationTests
         Locations = ConfigLocations.Default;
 
 #if DEBUG_IDISPOSABLE
+        View.DebugIDisposable = true;
         View.Instances.Clear ();
         RunState.Instances.Clear ();
 #endif
@@ -906,53 +908,6 @@ public class ApplicationTests
         Assert.Equal (3, count);
     }
 
-    // TODO: All Toplevel layout tests should be moved to ToplevelTests.cs
-    [Fact (Skip = "#2491 - Changing focus should cause NeedsDraw = true, so bogus test?")]
-    public void Run_Toplevel_With_Modal_View_Does_Not_Refresh_If_Not_Dirty ()
-    {
-        Init ();
-        var count = 0;
-
-        // Don't use Dialog here as it has more layout logic. Use Window instead.
-        Dialog d = null;
-        Toplevel top = new ();
-        top.DrawingContent += (s, a) => count++;
-        int iteration = -1;
-
-        Application.Iteration += (s, a) =>
-                                 {
-                                     iteration++;
-
-                                     if (iteration == 0)
-                                     {
-                                         // TODO: Don't use Dialog here as it has more layout logic. Use Window instead.
-                                         d = new ();
-                                         d.DrawingContent += (s, a) => count++;
-                                         Application.Run (d);
-                                     }
-                                     else if (iteration < 3)
-                                     {
-                                         Application.RaiseMouseEvent (new () { Flags = MouseFlags.ReportMousePosition });
-                                         Assert.False (top.NeedsDraw);
-                                         Assert.False (top.SubViewNeedsDraw);
-                                         Assert.False (top.NeedsLayout);
-                                         Assert.False (d.NeedsDraw);
-                                         Assert.False (d.SubViewNeedsDraw);
-                                         Assert.False (d.NeedsLayout);
-                                     }
-                                     else
-                                     {
-                                         Application.RequestStop ();
-                                     }
-                                 };
-        Application.Run (top);
-        top.Dispose ();
-        Application.Shutdown ();
-
-        // 1 - First top load, 1 - Dialog load, 1 - Dialog unload, Total - 3.
-        Assert.Equal (3, count);
-    }
-
     // TODO: All Toplevel layout tests should be moved to ToplevelTests.cs
     [Fact]
     public void Run_A_Modal_Toplevel_Refresh_Background_On_Moving ()

+ 2 - 1
UnitTests/Application/CursorTests.cs → Tests/UnitTests/Application/CursorTests.cs

@@ -1,4 +1,5 @@
-using Xunit.Abstractions;
+using UnitTests;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.ApplicationTests;
 

+ 4 - 116
UnitTests/Application/KeyboardTests.cs → Tests/UnitTests/Application/KeyboardTests.cs

@@ -1,4 +1,5 @@
-using Xunit.Abstractions;
+using UnitTests;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.ApplicationTests;
 
@@ -20,119 +21,6 @@ public class KeyboardTests
 
     private object _timeoutLock;
 
-    [Fact (Skip = "No longer valid test.")]
-    [AutoInitShutdown]
-    public void EnsuresTopOnFront_CanFocus_False_By_Keyboard ()
-    {
-        Toplevel top = new ();
-
-        var win = new Window
-        {
-            Title = "win",
-            X = 0,
-            Y = 0,
-            Width = 20,
-            Height = 10
-        };
-        var tf = new TextField { Width = 10 };
-        win.Add (tf);
-
-        var win2 = new Window
-        {
-            Title = "win2",
-            X = 22,
-            Y = 0,
-            Width = 20,
-            Height = 10
-        };
-        var tf2 = new TextField { Width = 10 };
-        win2.Add (tf2);
-        top.Add (win, win2);
-
-        Application.Begin (top);
-
-        Assert.True (win.CanFocus);
-        Assert.True (win.HasFocus);
-        Assert.True (win2.CanFocus);
-        Assert.False (win2.HasFocus);
-        Assert.Equal ("win", ((Window)top.Subviews [^1]).Title);
-
-        win.CanFocus = false;
-        Assert.False (win.CanFocus);
-        Assert.False (win.HasFocus);
-        Assert.True (win2.CanFocus);
-        Assert.True (win2.HasFocus);
-        Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title);
-
-        Application.RaiseKeyDownEvent (Key.F6);
-        Assert.True (win2.CanFocus);
-        Assert.False (win.HasFocus);
-        Assert.True (win2.CanFocus);
-        Assert.True (win2.HasFocus);
-        Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title);
-
-        Application.RaiseKeyDownEvent (Key.F6);
-        Assert.False (win.CanFocus);
-        Assert.False (win.HasFocus);
-        Assert.True (win2.CanFocus);
-        Assert.True (win2.HasFocus);
-        Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title);
-        top.Dispose ();
-    }
-
-    [Fact (Skip = "No longer valid test.")]
-    [AutoInitShutdown]
-    public void EnsuresTopOnFront_CanFocus_True_By_Keyboard ()
-    {
-        Toplevel top = new ();
-
-        var win = new Window
-        {
-            Title = "win",
-            X = 0,
-            Y = 0,
-            Width = 20,
-            Height = 10
-        };
-        var tf = new TextField { Width = 10 };
-        win.Add (tf);
-
-        var win2 = new Window
-        {
-            Title = "win2",
-            X = 22,
-            Y = 0,
-            Width = 20,
-            Height = 10
-        };
-        var tf2 = new TextField { Width = 10 };
-        win2.Add (tf2);
-        top.Add (win, win2);
-
-        Application.Begin (top);
-
-        Assert.True (win.CanFocus);
-        Assert.True (win.HasFocus);
-        Assert.True (win2.CanFocus);
-        Assert.False (win2.HasFocus);
-        Assert.Equal ("win", ((Window)top.Subviews [^1]).Title);
-
-        Application.RaiseKeyDownEvent (Key.F6);
-        Assert.True (win.CanFocus);
-        Assert.False (win.HasFocus);
-        Assert.True (win2.CanFocus);
-        Assert.True (win2.HasFocus);
-        Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title);
-
-        Application.RaiseKeyDownEvent (Key.F6);
-        Assert.True (win.CanFocus);
-        Assert.True (win.HasFocus);
-        Assert.True (win2.CanFocus);
-        Assert.False (win2.HasFocus);
-        Assert.Equal ("win", ((Window)top.Subviews [^1]).Title);
-        top.Dispose ();
-    }
-
     [Fact]
     [AutoInitShutdown]
     public void KeyBindings_Add_Adds ()
@@ -145,7 +33,7 @@ public class KeyboardTests
         Assert.True (Application.KeyBindings.TryGet (Key.B, out binding));
         Assert.Null (binding.Target);
     }
-            
+
     [Fact]
     [AutoInitShutdown]
     public void KeyBindings_Remove_Removes ()
@@ -161,7 +49,7 @@ public class KeyboardTests
     [Fact]
     public void KeyBindings_OnKeyDown ()
     {
-        Application.Top = new Toplevel ();
+        Application.Top = new ();
         var view = new ScopedKeyBindingView ();
         var keyWasHandled = false;
         view.KeyDownNotHandled += (s, e) => keyWasHandled = true;

+ 28 - 120
UnitTests/Application/MainLoopTests.cs → Tests/UnitTests/Application/MainLoopTests.cs

@@ -1,5 +1,4 @@
 using System.Diagnostics;
-using System.IO;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 
@@ -8,7 +7,6 @@ namespace Terminal.Gui.ApplicationTests;
 /// <summary>Tests MainLoop using the FakeMainLoop.</summary>
 public class MainLoopTests
 {
-    private static readonly ManualResetEventSlim _wakeUp = new (false);
     private static Button btn;
     private static string cancel;
     private static string clickMe;
@@ -22,28 +20,11 @@ public class MainLoopTests
     // - wait = false
 
     // TODO: Add IMainLoop tests
-    private static volatile int tbCounter;
     private static int three;
     private static int total;
     private static int two;
     private static int zero;
 
-    public static IEnumerable<object []> TestAddIdle
-    {
-        get
-        {
-            // Goes fine
-            Action a1 = StartWindow;
-
-            yield return new object [] { a1, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 };
-
-            // Also goes fine
-            Action a2 = () => Application.Invoke (StartWindow);
-
-            yield return new object [] { a2, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 };
-        }
-    }
-
     // See Also ConsoleDRivers/MainLoopDriverTests.cs for tests of the MainLoopDriver
 
     // Idle Handler tests
@@ -60,7 +41,7 @@ public class MainLoopTests
 
         Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count);
         Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]);
-        Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers[0]);
+        Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers [0]);
 
         Assert.True (ml.TimedEvents.RemoveIdle (fnTrue));
         Assert.Single (ml.TimedEvents.IdleHandlers);
@@ -82,15 +63,15 @@ public class MainLoopTests
         ml.AddIdle (fnTrue);
 
         Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count);
-        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]);
-        Assert.True (ml.TimedEvents.IdleHandlers[0] ());
-        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[1]);
-        Assert.True (ml.TimedEvents.IdleHandlers[1] ());
+        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]);
+        Assert.True (ml.TimedEvents.IdleHandlers [0] ());
+        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [1]);
+        Assert.True (ml.TimedEvents.IdleHandlers [1] ());
 
         Assert.True (ml.TimedEvents.RemoveIdle (fnTrue));
         Assert.Single (ml.TimedEvents.IdleHandlers);
-        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]);
-        Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers[0]);
+        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]);
+        Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers [0]);
 
         Assert.True (ml.TimedEvents.RemoveIdle (fnTrue));
         Assert.Empty (ml.TimedEvents.IdleHandlers);
@@ -298,10 +279,10 @@ public class MainLoopTests
         TimeoutEventArgs args = null;
 
         ml.TimedEvents.TimeoutAdded += (s, e) =>
-                           {
-                               sender = s;
-                               args = e;
-                           };
+                                       {
+                                           sender = s;
+                                           args = e;
+                                       };
 
         object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
 
@@ -616,41 +597,10 @@ public class MainLoopTests
         Assert.Empty (mainloop.TimedEvents.IdleHandlers);
 
         Assert.NotNull (
-                        new Timeout { Span = new TimeSpan (), Callback = () => true }
+                        new Timeout { Span = new (), Callback = () => true }
                        );
     }
 
-    [Theory]
-    [InlineData (typeof (FakeDriver))]
-    //[InlineData (typeof (NetDriver))] // BUGBUG: NetDriver never exits in this test
-
-    //[InlineData (typeof (ANSIDriver))]
-    //[InlineData (typeof (WindowsDriver))] // BUGBUG: NetDriver never exits in this test
-    //[InlineData (typeof (CursesDriver))] // BUGBUG: CursesDriver never exits in this test
-    public async Task InvokeLeakTest (Type driverType)
-    {
-        Application.Init (driverName: driverType.Name);
-        Random r = new ();
-        TextField tf = new ();
-        var top = new Toplevel ();
-        top.Add (tf);
-
-        const int numPasses = 2;
-        const int numIncrements = 500;
-        const int pollMs = 2500;
-
-        Task task = Task.Run (() => RunTest (r, tf, numPasses, numIncrements, pollMs));
-
-        // blocks here until the RequestStop is processed at the end of the test
-        Application.Run (top);
-
-        await task; // Propagate exception if any occurred
-
-        Assert.Equal (numIncrements * numPasses, tbCounter);
-        top.Dispose ();
-        Application.Shutdown ();
-    }
-
     [Theory]
     [MemberData (nameof (TestAddIdle))]
     public void Mainloop_Invoke_Or_AddIdle_Can_Be_Used_For_Events_Or_Actions (
@@ -666,7 +616,7 @@ public class MainLoopTests
     )
     {
         // TODO: Expand this test to test all drivers
-        Application.Init (new FakeDriver());
+        Application.Init (new FakeDriver ());
 
         total = 0;
         btn = null;
@@ -779,28 +729,20 @@ public class MainLoopTests
         Assert.Equal (10, functionCalled);
     }
 
-    private static void Launch (Random r, TextField tf, int target)
+    public static IEnumerable<object []> TestAddIdle
     {
-        Task.Run (
-                  () =>
-                  {
-                      Thread.Sleep (r.Next (2, 4));
-
-                      Application.Invoke (
-                                          () =>
-                                          {
-                                              tf.Text = $"index{r.Next ()}";
-                                              Interlocked.Increment (ref tbCounter);
-
-                                              if (target == tbCounter)
-                                              {
-                                                  // On last increment wake up the check
-                                                  _wakeUp.Set ();
-                                              }
-                                          }
-                                         );
-                  }
-                 );
+        get
+        {
+            // Goes fine
+            Action a1 = StartWindow;
+
+            yield return new object [] { a1, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 };
+
+            // Also goes fine
+            Action a2 = () => Application.Invoke (StartWindow);
+
+            yield return new object [] { a2, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 };
+        }
     }
 
     private static async void RunAsyncTest (object sender, EventArgs e)
@@ -862,40 +804,6 @@ public class MainLoopTests
                            );
     }
 
-    private static void RunTest (Random r, TextField tf, int numPasses, int numIncrements, int pollMs)
-    {
-        for (var j = 0; j < numPasses; j++)
-        {
-            _wakeUp.Reset ();
-
-            for (var i = 0; i < numIncrements; i++)
-            {
-                Launch (r, tf, (j + 1) * numIncrements);
-            }
-
-            while (tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
-            {
-                int tbNow = tbCounter;
-                _wakeUp.Wait (pollMs);
-
-                if (tbCounter == tbNow)
-                {
-                    // No change after wait: Idle handlers added via Application.Invoke have gone missing
-                    Application.Invoke (() => Application.RequestStop ());
-
-                    throw new TimeoutException (
-                                                $"Timeout: Increment lost. tbCounter ({tbCounter}) didn't "
-                                                + $"change after waiting {pollMs} ms. Failed to reach {(j + 1) * numIncrements} on pass {j + 1}"
-                                               );
-                }
-            }
-
-            ;
-        }
-
-        Application.Invoke (() => Application.RequestStop ());
-    }
-
     private static void SetReadyToRun ()
     {
         Thread.Sleep (100);
@@ -916,7 +824,7 @@ public class MainLoopTests
     {
         var startWindow = new Window { Modal = true };
 
-        btn = new Button { Text = "Click Me" };
+        btn = new() { Text = "Click Me" };
 
         btn.Accepting += RunAsyncTest;
 
@@ -937,8 +845,8 @@ public class MainLoopTests
 
     private class MillisecondTolerance : IEqualityComparer<TimeSpan>
     {
-        private readonly int _tolerance;
         public MillisecondTolerance (int tolerance) { _tolerance = tolerance; }
+        private readonly int _tolerance;
         public bool Equals (TimeSpan x, TimeSpan y) { return Math.Abs (x.Milliseconds - y.Milliseconds) <= _tolerance; }
         public int GetHashCode (TimeSpan obj) { return obj.GetHashCode (); }
     }

+ 0 - 0
UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs → Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs


+ 2 - 1
UnitTests/Application/Mouse/ApplicationMouseTests.cs → Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs

@@ -1,4 +1,5 @@
-using Xunit.Abstractions;
+using UnitTests;
+using Xunit.Abstractions;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 

+ 2 - 0
UnitTests/Application/RunStateTests.cs → Tests/UnitTests/Application/RunStateTests.cs

@@ -8,6 +8,8 @@ public class RunStateTests
     public RunStateTests ()
     {
 #if DEBUG_IDISPOSABLE
+        View.DebugIDisposable = true;
+
         View.Instances.Clear ();
         RunState.Instances.Clear ();
 #endif

+ 0 - 0
UnitTests/Application/StackExtensionsTests.cs → Tests/UnitTests/Application/StackExtensionsTests.cs


+ 2 - 0
UnitTests/Application/SynchronizatonContextTests.cs → Tests/UnitTests/Application/SynchronizatonContextTests.cs

@@ -1,5 +1,7 @@
 // Alias Console to MockConsole so we don't accidentally use Console
 
+using UnitTests;
+
 namespace Terminal.Gui.ApplicationTests;
 
 public class SyncrhonizationContextTests

+ 0 - 0
UnitTests/AssemblyInfo.cs → Tests/UnitTests/AssemblyInfo.cs


+ 140 - 0
Tests/UnitTests/AutoInitShutdownAttribute.cs

@@ -0,0 +1,140 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Reflection;
+using Xunit.Sdk;
+
+namespace UnitTests;
+
+/// <summary>
+///     Enables test functions annotated with the [AutoInitShutdown] attribute to
+///     automatically call Application.Init at start of the test and Application.Shutdown after the
+///     test exits.
+///     This is necessary because a) Application is a singleton and Init/Shutdown must be called
+///     as a pair, and b) all unit test functions should be atomic..
+/// </summary>
+[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)]
+public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
+{
+    /// <summary>
+    ///     Initializes a [AutoInitShutdown] attribute, which determines if/how Application.Init and Application.Shutdown
+    ///     are automatically called Before/After a test runs.
+    /// </summary>
+    /// <param name="autoInit">If true, Application.Init will be called Before the test runs.</param>
+    /// <param name="consoleDriverType">
+    ///     Determines which IConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver)
+    ///     will be used when Application.Init is called. If null FakeDriver will be used. Only valid if
+    ///     <paramref name="autoInit"/> is true.
+    /// </param>
+    /// <param name="useFakeClipboard">
+    ///     If true, will force the use of <see cref="FakeDriver.FakeClipboard"/>. Only valid if
+    ///     <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
+    /// </param>
+    /// <param name="fakeClipboardAlwaysThrowsNotSupportedException">
+    ///     Only valid if <paramref name="autoInit"/> is true. Only
+    ///     valid if <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
+    /// </param>
+    /// <param name="fakeClipboardIsSupportedAlwaysTrue">
+    ///     Only valid if <paramref name="autoInit"/> is true. Only valid if
+    ///     <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
+    /// </param>
+    /// <param name="configLocation">Determines what config file locations <see cref="ConfigurationManager"/> will load from.</param>
+    /// <param name="verifyShutdown">If true and <see cref="Application.Initialized"/> is true, the test will fail.</param>
+    public AutoInitShutdownAttribute (
+        bool autoInit = true,
+        Type consoleDriverType = null,
+        bool useFakeClipboard = true,
+        bool fakeClipboardAlwaysThrowsNotSupportedException = false,
+        bool fakeClipboardIsSupportedAlwaysTrue = false,
+        ConfigLocations configLocation = ConfigLocations.Default, // DefaultOnly is the default for tests
+        bool verifyShutdown = false
+    )
+    {
+        AutoInit = autoInit;
+        CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
+        _driverType = consoleDriverType ?? typeof (FakeDriver);
+        FakeDriver.FakeBehaviors.UseFakeClipboard = useFakeClipboard;
+
+        FakeDriver.FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException =
+            fakeClipboardAlwaysThrowsNotSupportedException;
+        FakeDriver.FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
+        ConfigurationManager.Locations = configLocation;
+        _verifyShutdown = verifyShutdown;
+    }
+
+    private readonly bool _verifyShutdown;
+    private readonly Type _driverType;
+
+    public override void After (MethodInfo methodUnderTest)
+    {
+        Debug.WriteLine ($"After: {methodUnderTest.Name}");
+
+        // Turn off diagnostic flags in case some test left them on
+        View.Diagnostics = ViewDiagnosticFlags.Off;
+
+        if (AutoInit)
+        {
+            // try
+            {
+                if (!_verifyShutdown)
+                {
+                    Application.ResetState (ignoreDisposed: true);
+                }
+
+                Application.Shutdown ();
+#if DEBUG_IDISPOSABLE
+                if (View.Instances.Count == 0)
+                {
+                    Assert.Empty (View.Instances);
+                }
+                else
+                {
+                    View.Instances.Clear ();
+                }
+#endif
+            }
+            //catch (Exception e)
+            //{
+            //    Assert.Fail ($"Application.Shutdown threw an exception after the test exited: {e}");
+            //}
+            //finally
+            {
+#if DEBUG_IDISPOSABLE
+                View.Instances.Clear ();
+                Application.ResetState (true);
+#endif
+            }
+        }
+
+        // Reset to defaults
+        ConfigurationManager.Locations = ConfigLocations.Default;
+        ConfigurationManager.Reset ();
+
+        // Enable subsequent tests that call Init to get all config files (the default).
+        //Locations = ConfigLocations.All;
+    }
+
+    public override void Before (MethodInfo methodUnderTest)
+    {
+        Debug.WriteLine ($"Before: {methodUnderTest.Name}");
+
+        if (AutoInit)
+        {
+#if DEBUG_IDISPOSABLE
+            View.DebugIDisposable = true;
+
+            // Clear out any lingering Responder instances from previous tests
+            if (View.Instances.Count == 0)
+            {
+                Assert.Empty (View.Instances);
+            }
+            else
+            {
+                View.Instances.Clear ();
+            }
+#endif
+            Application.Init ((IConsoleDriver)Activator.CreateInstance (_driverType));
+        }
+    }
+
+    private bool AutoInit { get; }
+}

+ 0 - 0
UnitTests/Clipboard/ClipboardTests.cs → Tests/UnitTests/Clipboard/ClipboardTests.cs


+ 1 - 0
UnitTests/Configuration/AppScopeTests.cs → Tests/UnitTests/Configuration/AppScopeTests.cs

@@ -1,4 +1,5 @@
 using System.Text.Json;
+using UnitTests;
 using static Terminal.Gui.ConfigurationManager;
 
 namespace Terminal.Gui.ConfigurationTests;

+ 1 - 0
UnitTests/Configuration/AttributeJsonConverterTests.cs → Tests/UnitTests/Configuration/AttributeJsonConverterTests.cs

@@ -1,4 +1,5 @@
 using System.Text.Json;
+using UnitTests;
 
 namespace Terminal.Gui.ConfigurationTests;
 

+ 0 - 0
UnitTests/Configuration/ColorJsonConverterTests.cs → Tests/UnitTests/Configuration/ColorJsonConverterTests.cs


+ 1 - 0
UnitTests/Configuration/ColorSchemeJsonConverterTests.cs → Tests/UnitTests/Configuration/ColorSchemeJsonConverterTests.cs

@@ -1,4 +1,5 @@
 using System.Text.Json;
+using UnitTests;
 
 namespace Terminal.Gui.ConfigurationTests;
 

+ 0 - 0
UnitTests/Configuration/ConfigPropertyTests.cs → Tests/UnitTests/Configuration/ConfigPropertyTests.cs


+ 1 - 0
UnitTests/Configuration/ConfigurationMangerTests.cs → Tests/UnitTests/Configuration/ConfigurationMangerTests.cs

@@ -1,6 +1,7 @@
 using System.Diagnostics;
 using System.Reflection;
 using System.Text.Json;
+using UnitTests;
 using Xunit.Abstractions;
 using static Terminal.Gui.ConfigurationManager;
 #pragma warning disable IDE1006

+ 0 - 0
UnitTests/Configuration/GlyphTests.cs → Tests/UnitTests/Configuration/GlyphTests.cs


+ 0 - 0
UnitTests/Configuration/KeyCodeJsonConverterTests.cs → Tests/UnitTests/Configuration/KeyCodeJsonConverterTests.cs


+ 0 - 0
UnitTests/Configuration/KeyJsonConverterTests.cs → Tests/UnitTests/Configuration/KeyJsonConverterTests.cs


+ 0 - 0
UnitTests/Configuration/RuneJsonConverterTests.cs → Tests/UnitTests/Configuration/RuneJsonConverterTests.cs


+ 0 - 0
UnitTests/Configuration/SerializableConfigurationPropertyTests.cs → Tests/UnitTests/Configuration/SerializableConfigurationPropertyTests.cs


+ 2 - 1
UnitTests/Configuration/SettingsScopeTests.cs → Tests/UnitTests/Configuration/SettingsScopeTests.cs

@@ -1,4 +1,5 @@
-using static Terminal.Gui.ConfigurationManager;
+using UnitTests;
+using static Terminal.Gui.ConfigurationManager;
 
 namespace Terminal.Gui.ConfigurationTests;
 

+ 1 - 0
UnitTests/Configuration/ThemeScopeTests.cs → Tests/UnitTests/Configuration/ThemeScopeTests.cs

@@ -1,4 +1,5 @@
 using System.Text.Json;
+using UnitTests;
 using static Terminal.Gui.ConfigurationManager;
 
 namespace Terminal.Gui.ConfigurationTests;

+ 1 - 0
UnitTests/Configuration/ThemeTests.cs → Tests/UnitTests/Configuration/ThemeTests.cs

@@ -1,4 +1,5 @@
 using System.Text.Json;
+using UnitTests;
 using static Terminal.Gui.ConfigurationManager;
 
 namespace Terminal.Gui.ConfigurationTests;

+ 4 - 4
UnitTests/ConsoleDrivers/AddRuneTests.cs → Tests/UnitTests/ConsoleDrivers/AddRuneTests.cs

@@ -68,22 +68,22 @@ public class AddRuneTests
 
         //		var s = "a\u0301\u0300\u0306";
 
-        //		TestHelpers.AssertDriverContentsWithFrameAre (@"
+        //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
         //ắ", output);
 
         //		tf.Text = "\u1eaf";
         //		Application.Refresh ();
-        //		TestHelpers.AssertDriverContentsWithFrameAre (@"
+        //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
         //ắ", output);
 
         //		tf.Text = "\u0103\u0301";
         //		Application.Refresh ();
-        //		TestHelpers.AssertDriverContentsWithFrameAre (@"
+        //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
         //ắ", output);
 
         //		tf.Text = "\u0061\u0306\u0301";
         //		Application.Refresh ();
-        //		TestHelpers.AssertDriverContentsWithFrameAre (@"
+        //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
         //ắ", output);
         driver.End ();
     }

+ 0 - 0
UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs → Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs → Tests/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs → Tests/UnitTests/ConsoleDrivers/AnsiRequestSchedulerTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs → Tests/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/ClipRegionTests.cs → Tests/UnitTests/ConsoleDrivers/ClipRegionTests.cs


+ 2 - 2
UnitTests/ConsoleDrivers/ConsoleDriverTests.cs → Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs

@@ -256,7 +256,7 @@ public class ConsoleDriverTests
     //└──────────────────┘
     //";
 
-    //					var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+    //					var pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output);
     //					Assert.Equal (new (0, 0, 20, 8), pos);
 
     //					Assert.True (dlg.ProcessKey (new (Key.Tab)));
@@ -273,7 +273,7 @@ public class ConsoleDriverTests
     //└──────────────────┘
     //";
 
-    //					pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+    //					pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output);
     //					Assert.Equal (new (0, 0, 20, 8), pos);
 
     //					win.RequestStop ();

+ 0 - 0
UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs → Tests/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/ConsoleScrolllingTests.cs → Tests/UnitTests/ConsoleDrivers/ConsoleScrolllingTests.cs


+ 8 - 6
UnitTests/ConsoleDrivers/ContentsTests.cs → Tests/UnitTests/ConsoleDrivers/ContentsTests.cs

@@ -1,4 +1,6 @@
 using System.Text;
+using UnitTests;
+using UnitTests;
 using Xunit.Abstractions;
 
 // Alias Console to MockConsole so we don't accidentally use Console
@@ -28,7 +30,7 @@ public class ContentsTests
         driver.Init ();
         var expected = "\u0301!";
         driver.AddStr ("\u0301!"); // acute accent + exclamation mark
-        TestHelpers.AssertDriverContentsAre (expected, output, driver);
+        DriverAssert.AssertDriverContentsAre (expected, output, driver);
 
         driver.End ();
     }
@@ -50,7 +52,7 @@ public class ContentsTests
         var expected = "é";
 
         driver.AddStr (combined);
-        TestHelpers.AssertDriverContentsAre (expected, output, driver);
+        DriverAssert.AssertDriverContentsAre (expected, output, driver);
 
         // 3 char combine
         // a + ogonek + acute = <U+0061, U+0328, U+0301> ( ą́ )
@@ -60,7 +62,7 @@ public class ContentsTests
 
         driver.Move (0, 0);
         driver.AddStr (combined);
-        TestHelpers.AssertDriverContentsAre (expected, output, driver);
+        DriverAssert.AssertDriverContentsAre (expected, output, driver);
 
         // e + ogonek + acute = <U+0061, U+0328, U+0301> ( ę́́ )
         combined = "e" + ogonek + acuteaccent;
@@ -68,7 +70,7 @@ public class ContentsTests
 
         driver.Move (0, 0);
         driver.AddStr (combined);
-        TestHelpers.AssertDriverContentsAre (expected, output, driver);
+        DriverAssert.AssertDriverContentsAre (expected, output, driver);
 
         // i + ogonek + acute = <U+0061, U+0328, U+0301> ( į́́́ )
         combined = "i" + ogonek + acuteaccent;
@@ -76,7 +78,7 @@ public class ContentsTests
 
         driver.Move (0, 0);
         driver.AddStr (combined);
-        TestHelpers.AssertDriverContentsAre (expected, output, driver);
+        DriverAssert.AssertDriverContentsAre (expected, output, driver);
 
         // u + ogonek + acute = <U+0061, U+0328, U+0301> ( ų́́́́ )
         combined = "u" + ogonek + acuteaccent;
@@ -84,7 +86,7 @@ public class ContentsTests
 
         driver.Move (0, 0);
         driver.AddStr (combined);
-        TestHelpers.AssertDriverContentsAre (expected, output, driver);
+        DriverAssert.AssertDriverContentsAre (expected, output, driver);
 
         driver.End ();
     }

+ 0 - 0
UnitTests/ConsoleDrivers/DriverColorTests.cs → Tests/UnitTests/ConsoleDrivers/DriverColorTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/KeyCodeTests.cs → Tests/UnitTests/ConsoleDrivers/KeyCodeTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/MainLoopDriverTests.cs → Tests/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs → Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs → Tests/UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs → Tests/UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs → Tests/UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs → Tests/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs → Tests/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs


+ 0 - 0
UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs → Tests/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs


部分文件因为文件数量过多而无法显示