Merge branch 'v2_develop' into v2_3574_self-contained-single-file

This commit is contained in:
BDisp
2024-07-07 21:58:13 +01:00
13 changed files with 625 additions and 583 deletions

View File

@@ -2,7 +2,7 @@ name: Build and publish API docs
on:
push:
branches: [main, develop, v2_develop]
branches: [main, v2_develop]
permissions:
id-token: write

View File

@@ -2,19 +2,24 @@ name: Build & Test Terminal.Gui with .NET Core
on:
push:
branches: [ main, develop, v2_develop ]
branches: [ v2_release, v2_develop ]
paths-ignore:
- '**.md'
pull_request:
branches: [ main, develop, v2_develop ]
branches: [ v2_release, v2_develop ]
paths-ignore:
- '**.md'
jobs:
build_and_test:
runs-on: windows-latest
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
- uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x
@@ -30,23 +35,23 @@ jobs:
- name: Test
run: |
sed -i 's/"stopOnFail": false/"stopOnFail": true/g' UnitTests/xunit.runner.json
dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" --settings UnitTests/coverlet.runsettings --blame
dotnet test --verbosity normal --blame
mv -v UnitTests/TestResults/*/*.* UnitTests/TestResults/
# 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: 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}}"
# - name: Print Code Coverage
# run: |
# echo "Code coverage percentage: ${{steps.create_coverage_badge.outputs.percentage}}%"
# echo "Badge data: ${{steps.create_coverage_badge.outputs.badge}}"

View File

@@ -25,7 +25,7 @@ jobs:
includePrerelease: true
- name: Determine Version
uses: gittools/actions/gitversion/execute@v0
uses: gittools/actions/gitversion/execute@v1
with:
useConfigFile: true
#additionalArguments: /b develop

View File

@@ -1,10 +1,10 @@
mode: ContinuousDeployment
tag-prefix: '[vV]'
continuous-delivery-fallback-tag: pre
continuous-delivery-fallback-tag: dev
branches:
develop:
mode: ContinuousDeployment
tag: pre
tag: dev
regex: develop
source-branches:
- main
@@ -12,28 +12,36 @@ branches:
v2_develop:
mode: ContinuousDeployment
tag: pre
tag: dev
regex: ^v2_develop?[/-]
is-release-branch: true
tracks-release-branches: true
#is-source-branch-for: ['v2']
is-source-branch-for: ['v2_release']
source-branches: []
main:
tag: rc
increment: Patch
source-branches:
- develop
- main
feature:
tag: useBranchName
regex: ^features?[/-]
source-branches:
- develop
- main
v2_release:
mode: ContinuousDeployment
tag: prealpha
regex: v2_release
is-release-branch: true
source-branches: ['v2_develop']
pull-request:
mode: ContinuousDeployment
tag: PullRequest.{BranchName}
increment: Inherit
tag-number-pattern: '[/-](?<number>\d+)'
regex: ^(pull|pull\-requests|pr)[/-]
source-branches:
- develop
- main
- release
- v2_develop
- v2_release
- feature
- support
- hotfix
pre-release-weight: 30000
ignore:
sha: []

View File

@@ -985,7 +985,7 @@ public static partial class Application
if (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ())
{
state.Toplevel.SetNeedsDisplay();
state.Toplevel.SetNeedsDisplay ();
state.Toplevel.Draw ();
Driver.UpdateScreen ();
@@ -1449,4 +1449,62 @@ public static partial class Application
}
#endregion Toplevel handling
/// <summary>
/// Gets a string representation of the Application as rendered by <see cref="Driver"/>.
/// </summary>
/// <returns>A string representation of the Application </returns>
public new static string ToString ()
{
ConsoleDriver driver = Driver;
if (driver is null)
{
return string.Empty;
}
return ToString (driver);
}
/// <summary>
/// Gets a string representation of the Application rendered by the provided <see cref="ConsoleDriver"/>.
/// </summary>
/// <param name="driver">The driver to use to render the contents.</param>
/// <returns>A string representation of the Application </returns>
public static string ToString (ConsoleDriver driver)
{
var sb = new StringBuilder ();
Cell [,] contents = driver.Contents;
for (var r = 0; r < driver.Rows; r++)
{
for (var c = 0; c < driver.Cols; c++)
{
Rune rune = contents [r, c].Rune;
if (rune.DecodeSurrogatePair (out char [] sp))
{
sb.Append (sp);
}
else
{
sb.Append ((char)rune.Value);
}
if (rune.GetColumns () > 1)
{
c++;
}
// See Issue #2616
//foreach (var combMark in contents [r, c].CombiningMarks) {
// sb.Append ((char)combMark.Value);
//}
}
sb.AppendLine ();
}
return sb.ToString ();
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System.Numerics;
using System.Text.Json.Serialization;
namespace Terminal.Gui;
@@ -13,28 +14,18 @@ namespace Terminal.Gui;
/// frame,
/// with the thickness widths subtracted.
/// </para>
/// <para>Use the helper API (<see cref="Draw(Rectangle, string)"/> to draw the frame with the specified thickness.</para>
/// <para>
/// Use the helper API (<see cref="Draw(Rectangle, string)"/> to draw the frame with the specified thickness.
/// </para>
/// <para>
/// Thickness uses <see langword="float"/> intenrally. As a result, there is a potential precision loss for very
/// large numbers. This is typically not an issue for UI dimensions but could be relevant in other contexts.
/// </para>
/// </remarks>
public class Thickness : IEquatable<Thickness>
public record struct Thickness
{
/// <summary>Gets or sets the width of the lower side of the rectangle.</summary>
[JsonInclude]
public int Bottom;
/// <summary>Gets or sets the width of the left side of the rectangle.</summary>
[JsonInclude]
public int Left;
/// <summary>Gets or sets the width of the right side of the rectangle.</summary>
[JsonInclude]
public int Right;
/// <summary>Gets or sets the width of the upper side of the rectangle.</summary>
[JsonInclude]
public int Top;
/// <summary>Initializes a new instance of the <see cref="Thickness"/> class with all widths set to 0.</summary>
public Thickness () { }
public Thickness () { _sides = Vector4.Zero; }
/// <summary>Initializes a new instance of the <see cref="Thickness"/> class with a uniform width to each side.</summary>
/// <param name="width"></param>
@@ -56,35 +47,23 @@ public class Thickness : IEquatable<Thickness>
Bottom = bottom;
}
// TODO: add operator overloads
/// <summary>Gets an empty thickness.</summary>
public static Thickness Empty => new (0);
private Vector4 _sides;
/// <summary>
/// Gets the total width of the left and right sides of the rectangle. Sets the width of the left and rigth sides
/// of the rectangle to half the specified value.
/// Adds the thickness widths of another <see cref="Thickness"/> to the current <see cref="Thickness"/>, returning a
/// new <see cref="Thickness"/>.
/// </summary>
public int Horizontal
{
get => Left + Right;
set => Left = Right = value / 2;
}
/// <summary>
/// Gets the total height of the top and bottom sides of the rectangle. Sets the height of the top and bottom
/// sides of the rectangle to half the specified value.
/// </summary>
public int Vertical
{
get => Top + Bottom;
set => Top = Bottom = value / 2;
}
// IEquitable
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other"></param>
/// <returns>true if the current object is equal to the other parameter; otherwise, false.</returns>
public bool Equals (Thickness other) { return other is { } && Left == other.Left && Right == other.Right && Top == other.Top && Bottom == other.Bottom; }
/// <returns></returns>
public readonly Thickness Add (Thickness other) { return new (Left + other.Left, Top + other.Top, Right + other.Right, Bottom + other.Bottom); }
/// <summary>Gets or sets the width of the lower side of the rectangle.</summary>
[JsonInclude]
public int Bottom
{
get => (int)_sides.W;
set => _sides.W = value;
}
/// <summary>
/// Gets whether the specified coordinates lie within the thickness (inside the bounding rectangle but outside
@@ -100,22 +79,6 @@ public class Thickness : IEquatable<Thickness>
return outside.Contains (location) && !inside.Contains (location);
}
/// <summary>
/// Adds the thickness widths of another <see cref="Thickness"/> to the current <see cref="Thickness"/>, returning a
/// new <see cref="Thickness"/>.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public Thickness Add (Thickness other) { return new (Left + other.Left, Top + other.Top, Right + other.Right, Bottom + other.Bottom); }
/// <summary>
/// Adds the thickness widths of another <see cref="Thickness"/> to another <see cref="Thickness"/>.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Thickness operator + (Thickness a, Thickness b) { return a.Add (b); }
/// <summary>Draws the <see cref="Thickness"/> rectangle with an optional diagnostics label.</summary>
/// <remarks>
/// If <see cref="ViewDiagnosticFlags"/> is set to
@@ -240,31 +203,8 @@ public class Thickness : IEquatable<Thickness>
return GetInside (rect);
}
/// <summary>Determines whether the specified object is equal to the current object.</summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
public override bool Equals (object obj)
{
//Check for null and compare run-time types.
if (obj is null || !GetType ().Equals (obj.GetType ()))
{
return false;
}
return Equals ((Thickness)obj);
}
/// <inheritdoc/>
public override int GetHashCode ()
{
var hashCode = 1380952125;
hashCode = hashCode * -1521134295 + Left.GetHashCode ();
hashCode = hashCode * -1521134295 + Right.GetHashCode ();
hashCode = hashCode * -1521134295 + Top.GetHashCode ();
hashCode = hashCode * -1521134295 + Bottom.GetHashCode ();
return hashCode;
}
/// <summary>Gets an empty thickness.</summary>
public static Thickness Empty => new (0);
/// <summary>
/// Returns a rectangle describing the location and size of the inside area of <paramref name="rect"/> with the
@@ -289,23 +229,59 @@ public class Thickness : IEquatable<Thickness>
return new (x, y, width, height);
}
/// <inheritdoc/>
public static bool operator == (Thickness left, Thickness right) { return EqualityComparer<Thickness>.Default.Equals (left, right); }
/// <summary>
/// Gets the total width of the left and right sides of the rectangle. Sets the width of the left and rigth sides
/// of the rectangle to half the specified value.
/// </summary>
public int Horizontal
{
get => Left + Right;
set => Left = Right = value / 2;
}
/// <inheritdoc/>
public static bool operator != (Thickness left, Thickness right) { return !(left == right); }
/// <summary>Gets or sets the width of the left side of the rectangle.</summary>
[JsonInclude]
public int Left
{
get => (int)_sides.X;
set => _sides.X = value;
}
/// <summary>
/// Adds the thickness widths of another <see cref="Thickness"/> to another <see cref="Thickness"/>.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Thickness operator + (Thickness a, Thickness b) { return a.Add (b); }
/// <summary>Gets or sets the width of the right side of the rectangle.</summary>
[JsonInclude]
public int Right
{
get => (int)_sides.Z;
set => _sides.Z = value;
}
/// <summary>Gets or sets the width of the upper side of the rectangle.</summary>
[JsonInclude]
public int Top
{
get => (int)_sides.Y;
set => _sides.Y = value;
}
/// <summary>Returns the thickness widths of the Thickness formatted as a string.</summary>
/// <returns>The thickness widths as a string.</returns>
public override string ToString () { return $"(Left={Left},Top={Top},Right={Right},Bottom={Bottom})"; }
private int validate (int width)
/// <summary>
/// Gets the total height of the top and bottom sides of the rectangle. Sets the height of the top and bottom
/// sides of the rectangle to half the specified value.
/// </summary>
public int Vertical
{
if (width < 0)
{
throw new ArgumentException ("Thickness widths cannot be negative.");
}
return width;
get => Top + Bottom;
set => Top = Bottom = value / 2;
}
}

View File

@@ -1,7 +1,5 @@
#nullable enable
using System.Drawing;
namespace Terminal.Gui;
/// <summary>The Margin for a <see cref="View"/>.</summary>
@@ -28,93 +26,43 @@ public class Margin : Adornment
CanFocus = false;
}
private void Margin_LayoutStarted (object? sender, LayoutEventArgs e)
{
// Adjust the shadow such that it is drawn aligned with the Border
if (ShadowStyle != Gui.ShadowStyle.None && _rightShadow is { } && _bottomShadow is { })
{
_rightShadow.Y = Parent.Border.Thickness.Top > 0 ? Parent.Border.Thickness.Top - (Parent.Border.Thickness.Top > 2 && Parent.Border.ShowTitle ? 1 : 0) : 1;
_bottomShadow.X = Parent.Border.Thickness.Left > 0 ? Parent.Border.Thickness.Left : 1;
}
}
private bool _pressed;
private void Margin_Highlight (object? sender, CancelEventArgs<HighlightStyle> e)
private ShadowView? _bottomShadow;
private ShadowView? _rightShadow;
/// <inheritdoc/>
public override void BeginInit ()
{
if (ShadowStyle != Gui.ShadowStyle.None)
base.BeginInit ();
if (Parent is null)
{
if (_pressed && e.CurrentValue == HighlightStyle.None)
{
Thickness = new (Thickness.Left - 1, Thickness.Top, Thickness.Right + 1, Thickness.Bottom);
if (_rightShadow is { })
{
_rightShadow.Visible = true;
}
if (_bottomShadow is { })
{
_bottomShadow.Visible = true;
}
_pressed = false;
return;
}
if (!_pressed && (e.CurrentValue.HasFlag (HighlightStyle.Pressed) /*|| e.HighlightStyle.HasFlag (HighlightStyle.PressedOutside)*/))
{
Thickness = new (Thickness.Left + 1, Thickness.Top, Thickness.Right - 1, Thickness.Bottom);
_pressed = true;
if (_rightShadow is { })
{
_rightShadow.Visible = false;
}
if (_bottomShadow is { })
{
_bottomShadow.Visible = false;
}
}
return;
}
}
ShadowStyle = base.ShadowStyle;
/// <inheritdoc />
public override void OnDrawContent (Rectangle viewport)
{
Rectangle screen = ViewportToScreen (viewport);
Attribute normalAttr = GetNormalColor ();
Driver?.SetAttribute (normalAttr);
// This just draws/clears the thickness, not the insides.
if (ShadowStyle != ShadowStyle.None)
{
screen = Rectangle.Inflate (screen, -1, -1);
}
Thickness.Draw (screen, ToString ());
if (Subviews.Count > 0)
{
// Draw subviews
// TODO: Implement OnDrawSubviews (cancelable);
if (Subviews is { } && SubViewNeedsDisplay)
{
IEnumerable<View> subviewsNeedingDraw = Subviews.Where (
view => view.Visible
&& (view.NeedsDisplay || view.SubViewNeedsDisplay || view.LayoutNeeded)
);
foreach (View view in subviewsNeedingDraw)
{
if (view.LayoutNeeded)
{
view.LayoutSubviews ();
}
view.Draw ();
}
}
}
Add (
_rightShadow = new()
{
X = Pos.AnchorEnd (1),
Y = 0,
Width = 1,
Height = Dim.Fill (),
ShadowStyle = ShadowStyle,
Orientation = Orientation.Vertical
},
_bottomShadow = new()
{
X = 0,
Y = Pos.AnchorEnd (1),
Width = Dim.Fill (),
Height = 1,
ShadowStyle = ShadowStyle,
Orientation = Orientation.Horizontal
}
);
}
/// <summary>
@@ -139,18 +87,48 @@ public class Margin : Adornment
}
}
/// <inheritdoc />
public override ShadowStyle ShadowStyle
/// <inheritdoc/>
public override void OnDrawContent (Rectangle viewport)
{
get => base.ShadowStyle;
set
Rectangle screen = ViewportToScreen (viewport);
Attribute normalAttr = GetNormalColor ();
Driver?.SetAttribute (normalAttr);
// This just draws/clears the thickness, not the insides.
if (ShadowStyle != ShadowStyle.None)
{
base.ShadowStyle = SetShadow (value);
screen = Rectangle.Inflate (screen, -1, -1);
}
Thickness.Draw (screen, ToString ());
if (Subviews.Count > 0)
{
// Draw subviews
// TODO: Implement OnDrawSubviews (cancelable);
if (Subviews is { } && SubViewNeedsDisplay)
{
IEnumerable<View> subviewsNeedingDraw = Subviews.Where (
view => view.Visible
&& (view.NeedsDisplay || view.SubViewNeedsDisplay || view.LayoutNeeded)
);
foreach (View view in subviewsNeedingDraw)
{
if (view.LayoutNeeded)
{
view.LayoutSubviews ();
}
view.Draw ();
}
}
}
}
/// <summary>
/// Sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the
/// Sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the
/// Margin.
/// </summary>
public ShadowStyle SetShadow (ShadowStyle style)
@@ -181,42 +159,73 @@ public class Margin : Adornment
{
_bottomShadow.ShadowStyle = style;
}
return style;
}
private ShadowView? _bottomShadow;
private ShadowView? _rightShadow;
/// <inheritdoc/>
public override void BeginInit ()
public override ShadowStyle ShadowStyle
{
base.BeginInit ();
if (Parent is null)
{
return;
}
ShadowStyle = base.ShadowStyle;
Add (
_rightShadow = new ShadowView
{
X = Pos.AnchorEnd (1),
Y = 0,
Width = 1,
Height = Dim.Fill (),
ShadowStyle = ShadowStyle,
Orientation = Orientation.Vertical
},
_bottomShadow = new ShadowView
{
X = 0,
Y = Pos.AnchorEnd (1),
Width = Dim.Fill (),
Height = 1,
ShadowStyle = ShadowStyle,
Orientation = Orientation.Horizontal
}
);
get => base.ShadowStyle;
set => base.ShadowStyle = SetShadow (value);
}
}
private void Margin_Highlight (object? sender, CancelEventArgs<HighlightStyle> e)
{
if (ShadowStyle != ShadowStyle.None)
{
if (_pressed && e.NewValue == HighlightStyle.None)
{
// If the view is pressed and the highlight is being removed, move the shadow back.
// Note, for visual effects reasons, we only move horizontally.
// TODO: Add a setting or flag that lets the view move vertically as well.
Thickness = new (Thickness.Left - 1, Thickness.Top, Thickness.Right + 1, Thickness.Bottom);
if (_rightShadow is { })
{
_rightShadow.Visible = true;
}
if (_bottomShadow is { })
{
_bottomShadow.Visible = true;
}
_pressed = false;
return;
}
if (!_pressed && e.NewValue.HasFlag (HighlightStyle.Pressed))
{
// If the view is not pressed and we want highlight move the shadow
// Note, for visual effects reasons, we only move horizontally.
// TODO: Add a setting or flag that lets the view move vertically as well.
Thickness = new (Thickness.Left + 1, Thickness.Top, Thickness.Right - 1, Thickness.Bottom);
_pressed = true;
if (_rightShadow is { })
{
_rightShadow.Visible = false;
}
if (_bottomShadow is { })
{
_bottomShadow.Visible = false;
}
}
}
}
private void Margin_LayoutStarted (object? sender, LayoutEventArgs e)
{
// Adjust the shadow such that it is drawn aligned with the Border
if (ShadowStyle != ShadowStyle.None && _rightShadow is { } && _bottomShadow is { })
{
_rightShadow.Y = Parent.Border.Thickness.Top > 0
? Parent.Border.Thickness.Top - (Parent.Border.Thickness.Top > 2 && Parent.Border.ShowTitle ? 1 : 0)
: 1;
_bottomShadow.X = Parent.Border.Thickness.Left > 0 ? Parent.Border.Thickness.Left : 1;
}
}
}

View File

@@ -1,32 +1,28 @@
using System.Collections;
using System.Diagnostics;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UICatalog;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Terminal.Gui;
// This class 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>
/// This class 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
{
private readonly Type _driverType;
/// <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="autoShutdown">If true, Application.Shutdown will be called After the test runs.</param>
/// <param name="consoleDriverType">
/// Determines which ConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver)
/// will be used when Application.Init is called. If null FakeDriver will be used. Only valid if
@@ -65,7 +61,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
ConfigurationManager.Locations = configLocation;
}
private bool AutoInit { get; }
private readonly Type _driverType;
public override void After (MethodInfo methodUnderTest)
{
@@ -102,6 +98,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
ConfigurationManager.Reset ();
#if DEBUG_IDISPOSABLE
// Clear out any lingering Responder instances from previous tests
if (Responder.Instances.Count == 0)
{
@@ -115,6 +112,8 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
Application.Init ((ConsoleDriver)Activator.CreateInstance (_driverType));
}
}
private bool AutoInit { get; }
}
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)]
@@ -178,8 +177,8 @@ public class SetupFakeDriverAttribute : BeforeAfterTestAttribute
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)]
public class TestDateAttribute : BeforeAfterTestAttribute
{
private readonly CultureInfo _currentCulture = CultureInfo.CurrentCulture;
public TestDateAttribute () { CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; }
private readonly CultureInfo _currentCulture = CultureInfo.CurrentCulture;
public override void After (MethodInfo methodUnderTest)
{
@@ -238,12 +237,12 @@ internal partial class TestHelpers
switch (match.Count)
{
case 0:
throw new Exception (
$"{DriverContentsToString (driver)}\n"
+ $"Expected Attribute {val} (PlatformColor = {val.Value.PlatformColor}) at Contents[{line},{c}] {contents [line, c]} ((PlatformColor = {contents [line, c].Attribute.Value.PlatformColor}) was not found.\n"
+ $" Expected: {string.Join (",", expectedAttributes.Select (c => c))}\n"
+ $" But Was: <not found>"
);
throw new (
$"{Application.ToString (driver)}\n"
+ $"Expected Attribute {val} (PlatformColor = {val.Value.PlatformColor}) at Contents[{line},{c}] {contents [line, c]} ((PlatformColor = {contents [line, c].Attribute.Value.PlatformColor}) was not found.\n"
+ $" Expected: {string.Join (",", expectedAttributes.Select (c => c))}\n"
+ $" But Was: <not found>"
);
case > 1:
throw new ArgumentException (
$"Bad value for expectedColors, {match.Count} Attributes had the same Value"
@@ -255,12 +254,12 @@ internal partial class TestHelpers
if (colorUsed != userExpected)
{
throw new Exception (
$"{DriverContentsToString (driver)}\n"
+ $"Unexpected Attribute at Contents[{line},{c}] {contents [line, c]}.\n"
+ $" Expected: {userExpected} ({expectedAttributes [int.Parse (userExpected.ToString ())]})\n"
+ $" But Was: {colorUsed} ({val})\n"
);
throw new (
$"{Application.ToString (driver)}\n"
+ $"Unexpected Attribute at Contents[{line},{c}] {contents [line, c]}.\n"
+ $" Expected: {userExpected} ({expectedAttributes [int.Parse (userExpected.ToString ())]})\n"
+ $" But Was: {colorUsed} ({val})\n"
);
}
}
@@ -282,7 +281,7 @@ internal partial class TestHelpers
)
{
#pragma warning restore xUnit1013 // Public method should be marked as test
string actualLook = DriverContentsToString (driver);
var actualLook = Application.ToString (driver ?? Application.Driver);
if (string.Equals (expectedLook, actualLook))
{
@@ -314,8 +313,7 @@ internal partial class TestHelpers
}
/// <summary>
/// Asserts that the driver contents are equal to the expected look, and that the cursor is at the expected
/// position.
/// Asserts that the driver contents are equal to the provided string.
/// </summary>
/// <param name="expectedLook"></param>
/// <param name="output"></param>
@@ -337,7 +335,6 @@ internal partial class TestHelpers
Cell [,] contents = driver.Contents;
for (var rowIndex = 0; rowIndex < driver.Rows; rowIndex++)
{
List<Rune> runes = [];
@@ -353,7 +350,7 @@ internal partial class TestHelpers
x = colIndex;
y = rowIndex;
for (int i = 0; i < colIndex; i++)
for (var i = 0; i < colIndex; i++)
{
runes.InsertRange (i, [SpaceRune]);
}
@@ -433,7 +430,7 @@ internal partial class TestHelpers
if (string.Equals (expectedLook, actualLook))
{
return new Rectangle (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
}
// standardize line endings for the comparison
@@ -453,7 +450,7 @@ internal partial class TestHelpers
Assert.Equal (expectedLook, actualLook);
return new Rectangle (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0);
}
#pragma warning disable xUnit1013 // Public method should be marked as test
@@ -483,278 +480,6 @@ internal partial class TestHelpers
}
#pragma warning restore xUnit1013 // Public method should be marked as test
public static string DriverContentsToString (ConsoleDriver driver = null)
{
var sb = new StringBuilder ();
driver ??= Application.Driver;
Cell [,] contents = driver.Contents;
for (var r = 0; r < driver.Rows; r++)
{
for (var c = 0; c < driver.Cols; c++)
{
Rune rune = contents [r, c].Rune;
if (rune.DecodeSurrogatePair (out char [] sp))
{
sb.Append (sp);
}
else
{
sb.Append ((char)rune.Value);
}
if (rune.GetColumns () > 1)
{
c++;
}
// See Issue #2616
//foreach (var combMark in contents [r, c].CombiningMarks) {
// sb.Append ((char)combMark.Value);
//}
}
sb.AppendLine ();
}
return sb.ToString ();
}
//// TODO: Update all tests that use GetALlViews to use GetAllViewsTheoryData instead
///// <summary>Gets a list of instances of all classes derived from View.</summary>
///// <returns>List of View objects</returns>
//public static List<View> GetAllViews ()
//{
// return typeof (View).Assembly.GetTypes ()
// .Where (
// type => type.IsClass
// && !type.IsAbstract
// && type.IsPublic
// && type.IsSubclassOf (typeof (View))
// )
// .Select (type => CreateView (type, type.GetConstructor (Array.Empty<Type> ())))
// .ToList ();
//}
//public class AllViewsData : IEnumerable<object []>
//{
// private Lazy<List<object []>> data;
// public AllViewsData ()
// {
// data = new Lazy<List<object []>> (GetTestData);
// }
// public IEnumerator<object []> GetEnumerator ()
// {
// return data.Value.GetEnumerator ();
// }
// IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
// private List<object []> GetTestData ()
// {
// var viewTypes = typeof (View).Assembly
// .GetTypes ()
// .Where (type => type.IsClass && !type.IsAbstract && type.IsPublic && type.IsSubclassOf (typeof (View)));
// var testData = new List<object []> ();
// foreach (var type in viewTypes)
// {
// var view = CreateView (type, type.GetConstructor (Array.Empty<Type> ()));
// testData.Add (new object [] { view, type.Name });
// }
// return testData;
// }
//}
/// <summary>
/// Verifies the console used all the <paramref name="expectedColors"/> when rendering. If one or more of the
/// expected colors are not used then the failure will output both the colors that were found to be used and which of
/// your expectations was not met.
/// </summary>
/// <param name="driver">if null uses <see cref="Application.Driver"/></param>
/// <param name="expectedColors"></param>
internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors)
{
driver ??= Application.Driver;
Cell [,] contents = driver.Contents;
List<Attribute> toFind = expectedColors.ToList ();
// Contents 3rd column is an Attribute
HashSet<Attribute> colorsUsed = new ();
for (var r = 0; r < driver.Rows; r++)
{
for (var c = 0; c < driver.Cols; c++)
{
Attribute? val = contents [r, c].Attribute;
if (val.HasValue)
{
colorsUsed.Add (val.Value);
Attribute match = toFind.FirstOrDefault (e => e == val);
// need to check twice because Attribute is a struct and therefore cannot be null
if (toFind.Any (e => e == val))
{
toFind.Remove (match);
}
}
}
}
if (!toFind.Any ())
{
return;
}
var sb = new StringBuilder ();
sb.AppendLine ("The following colors were not used:" + string.Join ("; ", toFind.Select (a => a.ToString ())));
sb.AppendLine ("Colors used were:" + string.Join ("; ", colorsUsed.Select (a => a.ToString ())));
throw new Exception (sb.ToString ());
}
private static void AddArguments (Type paramType, List<object> pTypes)
{
if (paramType == typeof (Rectangle))
{
pTypes.Add (Rectangle.Empty);
}
else if (paramType == typeof (string))
{
pTypes.Add (string.Empty);
}
else if (paramType == typeof (int))
{
pTypes.Add (0);
}
else if (paramType == typeof (bool))
{
pTypes.Add (true);
}
else if (paramType.Name == "IList")
{
pTypes.Add (new List<object> ());
}
else if (paramType.Name == "View")
{
var top = new Toplevel ();
var view = new View ();
top.Add (view);
pTypes.Add (view);
}
else if (paramType.Name == "View[]")
{
pTypes.Add (new View [] { });
}
else if (paramType.Name == "Stream")
{
pTypes.Add (new MemoryStream ());
}
else if (paramType.Name == "String")
{
pTypes.Add (string.Empty);
}
else if (paramType.Name == "TreeView`1[T]")
{
pTypes.Add (string.Empty);
}
else
{
pTypes.Add (null);
}
}
public static View CreateView (Type type, ConstructorInfo ctor)
{
View view = null;
if (type.IsGenericType && type.IsTypeDefinition)
{
List<Type> gTypes = new ();
foreach (Type args in type.GetGenericArguments ())
{
gTypes.Add (typeof (object));
}
type = type.MakeGenericType (gTypes.ToArray ());
Assert.IsType (type, (View)Activator.CreateInstance (type));
}
else
{
ParameterInfo [] paramsInfo = ctor.GetParameters ();
Type paramType;
List<object> pTypes = new ();
if (type.IsGenericType)
{
foreach (Type args in type.GetGenericArguments ())
{
paramType = args.GetType ();
if (args.Name == "T")
{
pTypes.Add (typeof (object));
}
else
{
AddArguments (paramType, pTypes);
}
}
}
foreach (ParameterInfo p in paramsInfo)
{
paramType = p.ParameterType;
if (p.HasDefaultValue)
{
pTypes.Add (p.DefaultValue);
}
else
{
AddArguments (paramType, pTypes);
}
}
if (type.IsGenericType && !type.IsTypeDefinition)
{
view = (View)Activator.CreateInstance (type);
Assert.IsType (type, view);
}
else
{
view = (View)ctor.Invoke (pTypes.ToArray ());
Assert.IsType (type, view);
}
}
return view;
}
public static List<Type> GetAllViewClasses ()
{
return typeof (View).Assembly.GetTypes ()
.Where (
myType => myType.IsClass
&& !myType.IsAbstract
&& myType.IsPublic
&& myType.IsSubclassOf (typeof (View))
)
.ToList ();
}
public static View CreateViewFromType (Type type, ConstructorInfo ctor)
{
View viewType = null;
@@ -824,6 +549,119 @@ internal partial class TestHelpers
return viewType;
}
public static List<Type> GetAllViewClasses ()
{
return typeof (View).Assembly.GetTypes ()
.Where (
myType => myType.IsClass
&& !myType.IsAbstract
&& myType.IsPublic
&& myType.IsSubclassOf (typeof (View))
)
.ToList ();
}
/// <summary>
/// Verifies the console used all the <paramref name="expectedColors"/> when rendering. If one or more of the
/// expected colors are not used then the failure will output both the colors that were found to be used and which of
/// your expectations was not met.
/// </summary>
/// <param name="driver">if null uses <see cref="Application.Driver"/></param>
/// <param name="expectedColors"></param>
internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors)
{
driver ??= Application.Driver;
Cell [,] contents = driver.Contents;
List<Attribute> toFind = expectedColors.ToList ();
// Contents 3rd column is an Attribute
HashSet<Attribute> colorsUsed = new ();
for (var r = 0; r < driver.Rows; r++)
{
for (var c = 0; c < driver.Cols; c++)
{
Attribute? val = contents [r, c].Attribute;
if (val.HasValue)
{
colorsUsed.Add (val.Value);
Attribute match = toFind.FirstOrDefault (e => e == val);
// need to check twice because Attribute is a struct and therefore cannot be null
if (toFind.Any (e => e == val))
{
toFind.Remove (match);
}
}
}
}
if (!toFind.Any ())
{
return;
}
var sb = new StringBuilder ();
sb.AppendLine ("The following colors were not used:" + string.Join ("; ", toFind.Select (a => a.ToString ())));
sb.AppendLine ("Colors used were:" + string.Join ("; ", colorsUsed.Select (a => a.ToString ())));
throw new (sb.ToString ());
}
private static void AddArguments (Type paramType, List<object> pTypes)
{
if (paramType == typeof (Rectangle))
{
pTypes.Add (Rectangle.Empty);
}
else if (paramType == typeof (string))
{
pTypes.Add (string.Empty);
}
else if (paramType == typeof (int))
{
pTypes.Add (0);
}
else if (paramType == typeof (bool))
{
pTypes.Add (true);
}
else if (paramType.Name == "IList")
{
pTypes.Add (new List<object> ());
}
else if (paramType.Name == "View")
{
var top = new Toplevel ();
var view = new View ();
top.Add (view);
pTypes.Add (view);
}
else if (paramType.Name == "View[]")
{
pTypes.Add (new View [] { });
}
else if (paramType.Name == "Stream")
{
pTypes.Add (new MemoryStream ());
}
else if (paramType.Name == "String")
{
pTypes.Add (string.Empty);
}
else if (paramType.Name == "TreeView`1[T]")
{
pTypes.Add (string.Empty);
}
else
{
pTypes.Add (null);
}
}
[GeneratedRegex ("^\\s+", RegexOptions.Multiline)]
private static partial Regex LeadingWhitespaceRegEx ();
@@ -832,11 +670,11 @@ internal partial class TestHelpers
string replaced = toReplace;
replaced = Environment.NewLine.Length switch
{
2 when !replaced.Contains ("\r\n") => replaced.Replace ("\n", Environment.NewLine),
1 => replaced.Replace ("\r\n", Environment.NewLine),
var _ => replaced
};
{
2 when !replaced.Contains ("\r\n") => replaced.Replace ("\n", Environment.NewLine),
1 => replaced.Replace ("\r\n", Environment.NewLine),
var _ => replaced
};
return replaced;
}
@@ -863,6 +701,4 @@ public class TestsAllViews
return Activator.CreateInstance (type) as View;
}
}

25
UnitTests/UnitTests.sln Normal file
View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests.csproj", "{A29633F2-B26E-48B2-997A-1733286E3C13}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A29633F2-B26E-48B2-997A-1733286E3C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A29633F2-B26E-48B2-997A-1733286E3C13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A29633F2-B26E-48B2-997A-1733286E3C13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A29633F2-B26E-48B2-997A-1733286E3C13}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {86ED8EAD-F1D5-4F95-A0E6-6D73DFC8442F}
EndGlobalSection
EndGlobal

View File

@@ -18,7 +18,7 @@ public class BorderTests (ITestOutputHelper output)
view.Border.Thickness = new (0, 1, 0, 0);
view.Border.LineStyle = LineStyle.Single;
view.ColorScheme = new()
view.ColorScheme = new ()
{
Normal = new (Color.Red, Color.Green),
Focus = new (Color.Green, Color.Red)
@@ -53,7 +53,7 @@ public class BorderTests (ITestOutputHelper output)
view.Border.Thickness = new (0, 1, 0, 0);
view.Border.LineStyle = LineStyle.Single;
view.ColorScheme = new()
view.ColorScheme = new ()
{
Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red)
};
@@ -90,7 +90,7 @@ public class BorderTests (ITestOutputHelper output)
{
Title = "1234", Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Double
};
win.Border.Thickness.Top = 4;
win.Border.Thickness = win.Border.Thickness with { Top = 4 };
RunState rs = Application.Begin (win);
var firstIteration = false;
@@ -224,7 +224,7 @@ public class BorderTests (ITestOutputHelper output)
{
Title = "1234", Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Double
};
win.Border.Thickness.Top = 3;
win.Border.Thickness = win.Border.Thickness with { Top = 3 };
RunState rs = Application.Begin (win);
var firstIteration = false;
@@ -358,7 +358,7 @@ public class BorderTests (ITestOutputHelper output)
{
Title = "1234", Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Double
};
win.Border.Thickness.Top = 2;
win.Border.Thickness = win.Border.Thickness with { Top = 2 };
RunState rs = Application.Begin (win);
var firstIteration = false;

View File

@@ -10,13 +10,13 @@ public class MarginTests (ITestOutputHelper output)
{
((FakeDriver)Application.Driver).SetBufferSize (5, 5);
var view = new View { Height = 3, Width = 3 };
view.Margin.Thickness = new Thickness (1);
view.Margin.Thickness = new (1);
var superView = new View ();
superView.ColorScheme = new ColorScheme
superView.ColorScheme = new()
{
Normal = new Attribute (Color.Red, Color.Green), Focus = new Attribute (Color.Green, Color.Red)
Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red)
};
superView.Add (view);

View File

@@ -27,6 +27,132 @@ public class ShadowStyleTests (ITestOutputHelper _output)
view.Dispose ();
}
[Theory]
[InlineData (ShadowStyle.None, 0, 0, 0, 0)]
[InlineData (ShadowStyle.Opaque, 1, 0, 0, 1)]
[InlineData (ShadowStyle.Transparent, 1, 0, 0, 1)]
public void ShadowStyle_Button1Pressed_Causes_Movement (ShadowStyle style, int expectedLeft, int expectedTop, int expectedRight, int expectedBottom)
{
var superView = new View
{
Height = 10, Width = 10
};
View view = new ()
{
Width = Dim.Auto (),
Height = Dim.Auto (),
Text = "0123",
HighlightStyle = HighlightStyle.Pressed,
ShadowStyle = style,
CanFocus = true
};
superView.Add (view);
superView.BeginInit ();
superView.EndInit ();
Thickness origThickness = view.Margin.Thickness;
view.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed, Position = new (0, 0) });
Assert.Equal (new (expectedLeft, expectedTop, expectedRight, expectedBottom), view.Margin.Thickness);
view.NewMouseEvent (new () { Flags = MouseFlags.Button1Released, Position = new (0, 0) });
Assert.Equal (origThickness, view.Margin.Thickness);
}
[Theory]
[InlineData (ShadowStyle.None, 0, 0, 0, 0)]
[InlineData (ShadowStyle.Opaque, 0, 0, 1, 1)]
[InlineData (ShadowStyle.Transparent, 0, 0, 1, 1)]
public void ShadowStyle_Margin_Thickness (ShadowStyle style, int expectedLeft, int expectedTop, int expectedRight, int expectedBottom)
{
var superView = new View
{
Height = 10, Width = 10
};
View view = new ()
{
Width = Dim.Auto (),
Height = Dim.Auto (),
Text = "0123",
HighlightStyle = HighlightStyle.Pressed,
ShadowStyle = style,
CanFocus = true
};
superView.Add (view);
superView.BeginInit ();
superView.EndInit ();
Assert.Equal (new (expectedLeft, expectedTop, expectedRight, expectedBottom), view.Margin.Thickness);
}
[Theory]
[InlineData (
ShadowStyle.None,
"""
011
111
111
""")]
[InlineData (
ShadowStyle.Transparent,
"""
011
131
111
""")]
[InlineData (
ShadowStyle.Opaque,
"""
011
121
111
""")]
[SetupFakeDriver]
public void ShadowView_Colors (ShadowStyle style, string expectedAttrs)
{
Color fg = Color.Red;
Color bg = Color.Green;
// 0 - View
// 1 - SuperView
// 2 - Opaque - fg is Black, bg is SuperView.Bg
// 3 - Transparent - fg is darker fg, bg is darker bg
Attribute [] attributes =
{
Attribute.Default,
new (fg, bg),
new (Color.Black, bg),
new (fg.GetDarkerColor (), bg.GetDarkerColor ())
};
var superView = new View
{
Height = 3,
Width = 3,
Text = "012ABC!@#",
ColorScheme = new (new Attribute (fg, bg))
};
superView.TextFormatter.WordWrap = true;
View view = new ()
{
Width = Dim.Auto (),
Height = Dim.Auto (),
Text = " ",
ShadowStyle = style,
ColorScheme = new (Attribute.Default)
};
superView.Add (view);
superView.BeginInit ();
superView.EndInit ();
superView.Draw ();
TestHelpers.AssertDriverAttributesAre (expectedAttrs, Application.Driver, attributes);
}
[Theory]
[InlineData (ShadowStyle.None, 3)]
[InlineData (ShadowStyle.Opaque, 4)]

View File

@@ -647,5 +647,4 @@ public class MouseTests (ITestOutputHelper output) : TestsAllViews
}
}
}
}