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 <bd.bdisp@gmail.com>
This commit is contained in:
Tig
2025-03-05 23:44:27 -07:00
committed by GitHub
parent 64b216b1e8
commit b0f32811eb
303 changed files with 11531 additions and 11883 deletions

34
.github/workflows/build-release.yml vendored Normal file
View File

@@ -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
.github/workflows/check-duplicates.yml vendored Normal file
View File

@@ -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"

View File

@@ -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/dotnet-code-coverage-badge@v1.0.0
# 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
.github/workflows/integration-tests.yml vendored Normal file
View File

@@ -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
.github/workflows/stress-tests.yml vendored Normal file
View File

@@ -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
.github/workflows/unit-tests.yml vendored Normal file
View File

@@ -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
.gitignore vendored
View File

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

View File

@@ -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>

View File

@@ -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>

11
Directory.Build.props Normal file
View File

@@ -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
Directory.Packages.props Normal file
View File

@@ -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>

View File

@@ -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. -->

View File

@@ -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: []

View File

@@ -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>

View File

@@ -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"
]
}
}

View File

@@ -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" />

View File

@@ -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"
]
}
}

BIN
Scripts/.testloop.sh.swp Normal file

Binary file not shown.

View File

@@ -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
}

View File

@@ -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 its 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
Scripts/testloop.sh Executable file
View File

@@ -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

View File

@@ -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>

View File

@@ -62,7 +62,10 @@ public static partial class Application // Mouse handling
}
#if DEBUG_IDISPOSABLE
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);
}

View File

@@ -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.
/// 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 ();
@@ -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)

View File

@@ -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 ();

View File

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

View File

@@ -22,7 +22,10 @@ public class RunState : IDisposable
Dispose (true);
GC.SuppressFinalize (this);
#if DEBUG_IDISPOSABLE
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
}

View File

@@ -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)

View File

@@ -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}");
}

View File

@@ -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
{

View File

@@ -12,22 +12,24 @@
<!-- Assembly name. -->
<!-- Referenced throughout this file for consistency. -->
<!-- =================================================================== -->
<PropertyGroup>
<PropertyGroup>
<AssemblyName>Terminal.Gui</AssemblyName>
</PropertyGroup>
</PropertyGroup>
<!-- =================================================================== -->
<!-- .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>
<ImplicitUsings>enable</ImplicitUsings>
<NoLogo>true</NoLogo>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
@@ -53,17 +55,17 @@
<!-- 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" />
<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" 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)" />
<PackageReference Include="Microsoft.SourceLink.GitHub" />
</ItemGroup>
<!-- =================================================================== -->
<!-- Global Usings and Type Aliases -->
@@ -80,6 +82,9 @@
<!-- =================================================================== -->
<ItemGroup>
<InternalsVisibleTo Include="UnitTests" />
<InternalsVisibleTo Include="UnitTests.Parallelizable" />
<InternalsVisibleTo Include="StressTests" />
<InternalsVisibleTo Include="IntegrationTests" />
<InternalsVisibleTo Include="TerminalGuiDesigner" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>
@@ -144,7 +149,11 @@
<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>
<ProjectExtensions>
<VisualStudio>
<UserProperties resources_4config_1json__JsonSchema="../../docfx/schemas/tui-config-schema.json" />
</VisualStudio>
</ProjectExtensions>
<Target Name="CopyNuGetPackagesToLocalPackagesFolder" AfterTargets="Pack" Condition="'$(Configuration)' == 'Release'">
<PropertyGroup>

View File

@@ -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

View File

@@ -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>

View File

@@ -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)
{

View File

@@ -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
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; }

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,168 +141,6 @@ 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
@@ -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 ()
{

29
Tests/README.md Normal file
View File

@@ -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.

View File

@@ -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 ();
}
}
);
}
);
}
}
}

View File

@@ -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 });
}

View File

@@ -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>

View File

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

View File

@@ -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);

View File

@@ -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 ()

View File

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

View File

@@ -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 ()
@@ -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;

View File

@@ -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);
@@ -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,29 +729,21 @@ public class MainLoopTests
Assert.Equal (10, functionCalled);
}
private static void Launch (Random r, TextField tf, int target)
public static IEnumerable<object []> TestAddIdle
{
Task.Run (
() =>
get
{
Thread.Sleep (r.Next (2, 4));
// Goes fine
Action a1 = StartWindow;
Application.Invoke (
() =>
{
tf.Text = $"index{r.Next ()}";
Interlocked.Increment (ref tbCounter);
yield return new object [] { a1, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 };
if (target == tbCounter)
{
// On last increment wake up the check
_wakeUp.Set ();
// 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 (); }
}

View File

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

View File

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

View File

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

View File

@@ -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; }
}

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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 ();
}

View File

@@ -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 ();

View File

@@ -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 ();
}

Some files were not shown because too many files have changed in this diff Show More