diff --git a/Arcade.slnx b/Arcade.slnx index ad6355b7062..1e9aa9aae67 100644 --- a/Arcade.slnx +++ b/Arcade.slnx @@ -33,6 +33,7 @@ + @@ -72,6 +73,7 @@ + diff --git a/Documentation/update-xunit.md b/Documentation/update-xunit.md index 3aab0389da9..270eaa1f6ed 100644 --- a/Documentation/update-xunit.md +++ b/Documentation/update-xunit.md @@ -3,4 +3,6 @@ This document aims to establish the necessary actions to update the xunit versio 1. For security reasons, nuget packages need to be manually mirrored from nuget.org to the dotnet-public AzDO feed. [See the instructions](/Documentation/MirroringPackages.md). Mirror the following xunit packages: `xunit,xunit.console,xunit.runner.reporters,xunit.runner.utility,xunit.runner.console,xunit.runner.visualstudio` with version `latest`. 2. Update `XUnitVersion`, `XUnitAnalyzersVersion`, `XUnitRunnerVisualStudioVersion`, `XUnitV3Version` and `MicrosoftTestingPlatformVersion` properties in [Arcade SDK's DefaultVersions.props](/src/Microsoft.DotNet.Arcade.Sdk/tools/DefaultVersions.props) to the desired values. Make sure to use a coherent version of `xunit.analyzers`. Note that `MicrosoftTestingPlatformVersion` must be compatible with the `XUnitV3Version` (xunit.v3.mtp-v1 depends on a specific minimum version of Microsoft.Testing.Platform). 3. Update other hardcoded values of `XUnitVersion` inside the Arcade repository (i.e. in [SendingJobsToHelix.md](/Documentation/AzureDevOps/SendingJobsToHelix.md), [Directory.Packages.props](/Directory.Packages.props) and others). -4. Submit a Pull request with these changes to [dotnet/arcade](https://github.com/dotnet/arcade). +4. Update Microsoft.DotNet.XUnitAssert which is an AOT compatible fork of the xunit.assert library by following [the instructions](/src/Microsoft.DotNet.XUnitAssert/README.md). It's likely that new XUnit versions introduce AOT incompatibilities which will cause the compiler (AOT analyzer) to fail. If you aren't sure how to resolve the errors, consult with @agocke's team who owns this library. +5. Submit a Pull request with these changes to [dotnet/arcade](https://github.com/dotnet/arcade) and tag @ViktorHofer as a reviewer. +6. Update the build from source compatible xunit/xunit fork in the [dotnet/source-build-reference-packages](https://github.com/dotnet/source-build-reference-packages/tree/main/src/externalPackages) repository by following [the instructions](https://github.com/dotnet/source-build-reference-packages) and submit a Pull Request. diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT index 3dbc4c4b7c2..ec48c2cc1a7 100644 --- a/THIRD-PARTY-NOTICES.TXT +++ b/THIRD-PARTY-NOTICES.TXT @@ -10,6 +10,26 @@ The attached notices are provided for information only. --------------------------------------- +The code in src/Microsoft.DotNet.XUnitAssert/src/* was imported from: + https://github.com/xunit/assert.xunit + +This set of code is covered by the following license: + + Copyright (c) .NET Foundation and Contributors + All Rights Reserved + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + The code in src/Microsoft.DotNet.XUnitConsoleRunner/src/* was imported from: https://github.com/xunit/xunit/tree/v2/src/xunit.console diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnit/XUnit.targets b/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnit/XUnit.targets index 74d8b5189b8..f3bee3148e5 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnit/XUnit.targets +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnit/XUnit.targets @@ -8,8 +8,8 @@ - - + + diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnitV3/XUnitV3.targets b/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnitV3/XUnitV3.targets index 578d760106d..f39adb984b1 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnitV3/XUnitV3.targets +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/XUnitV3/XUnitV3.targets @@ -11,8 +11,8 @@ - - + + diff --git a/src/Microsoft.DotNet.XUnitAssert/README.md b/src/Microsoft.DotNet.XUnitAssert/README.md new file mode 100644 index 00000000000..e1c15c635c1 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/README.md @@ -0,0 +1,22 @@ +# Custom Version of Xunit Assert + +## Origin/Attribution + +This is a fork of the code in https://github.com/xunit/assert.xunit for building the +`Microsoft.DotNet.XUnitAssert` NuGet package. See `../../THIRD-PARTY-NOTICES.TXT` for the license for this code. + +## Updating + +This repository is a "github subtree" of the assert.xunit repo. Follow these steps to update the code: + +1. Find what version you want to update to. This can be a tag or a commit on the assert.xunit repo. +2. Run the pull command. From the root of the repo run: `git subtree pull --squash --prefix src/Microsoft.DotNet.XUnitAssert/src https://github.com/xunit/assert.xunit ` +3. Resolve merge commits. +4. Commit the result. +5. Get someone with admin permissions to **Merge** (not squash or rebase) the results. Git subtree does not like squash. + +## Purpose + +This copy of assert.xunit is intended to be AOT-compatible and contains breaking changes from the +original code. In general, code which relied on reflection or dynamic code generation has been +removed in this fork. diff --git a/src/Microsoft.DotNet.XUnitAssert/src/.editorconfig b/src/Microsoft.DotNet.XUnitAssert/src/.editorconfig new file mode 100644 index 00000000000..c6af9b3bdb5 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/.editorconfig @@ -0,0 +1,228 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab + +[*.sln] +end_of_line = crlf + +# Visual Studio demands 2-spaced project files +# Tabs are not legal whitespace for YAML files +[*.{csproj,json,props,targets,xslt,yaml,yml}] +indent_style = space +indent_size = 2 + +[*.cs] +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = false +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = true +csharp_style_expression_bodied_methods = true +csharp_style_expression_bodied_operators = true +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async + +# Code-block preferences +csharp_prefer_braces = false +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = file_scoped + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:warning + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +#### Code quality rules #### + +# Analyzer severity + +dotnet_diagnostic.IDE0040.severity = none # Add accessibility modifiers +dotnet_diagnostic.IDE0079.severity = none # Remove unnecessary suppression diff --git a/src/Microsoft.DotNet.XUnitAssert/src/.gitattributes b/src/Microsoft.DotNet.XUnitAssert/src/.gitattributes new file mode 100644 index 00000000000..f062fc07828 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/.gitattributes @@ -0,0 +1,6 @@ +* text=auto eol=lf +*.cs text diff=csharp +*.csproj text merge=union +*.resx text merge=union +*.sln text eol=crlf merge=union +*.vbproj text merge=union diff --git a/src/Microsoft.DotNet.XUnitAssert/src/.github/FUNDING.yml b/src/Microsoft.DotNet.XUnitAssert/src/.github/FUNDING.yml new file mode 100644 index 00000000000..0a99d30a482 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/.github/FUNDING.yml @@ -0,0 +1 @@ +github: xunit diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Assert.cs b/src/Microsoft.DotNet.XUnitAssert/src/Assert.cs new file mode 100644 index 00000000000..3e9d4a26ff8 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Assert.cs @@ -0,0 +1,76 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#endif + +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Xunit +{ + /// + /// Contains various static methods that are used to verify that conditions are met during the + /// process of running tests. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + static readonly Type typeofDictionary = typeof(Dictionary<,>); + static readonly Type typeofHashSet = typeof(HashSet<>); + + /// + /// Initializes a new instance of the class. + /// + protected Assert() { } + + /// Do not call this method. + [Obsolete("This is an override of Object.Equals(). Call Assert.Equal() instead.", true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static new bool Equals( + object a, + object b) + { + throw new InvalidOperationException("Assert.Equals should not be used"); + } + + /// Do not call this method. + [Obsolete("This is an override of Object.ReferenceEquals(). Call Assert.Same() instead.", true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static new bool ReferenceEquals( + object a, + object b) + { + throw new InvalidOperationException("Assert.ReferenceEquals should not be used"); + } + + /// + /// Safely perform , returning when the + /// type is not generic. + /// + /// The potentially generic type + /// The generic type definition, when is generic; , otherwise. +#if XUNIT_NULLABLE + static Type? SafeGetGenericTypeDefinition(Type? type) +#else + static Type SafeGetGenericTypeDefinition(Type type) +#endif + { + if (type == null) + return null; + + if (!type.IsGenericType) + return null; + + return type.GetGenericTypeDefinition(); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/AsyncCollectionAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/AsyncCollectionAsserts.cs new file mode 100644 index 00000000000..1e3b915b66e --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/AsyncCollectionAsserts.cs @@ -0,0 +1,464 @@ +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA1720 // Identifier contains type name + +#if NET8_0_OR_GREATER + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit.Internal; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that all items in the collection pass when executed against + /// action. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static void All( + IAsyncEnumerable collection, + Action action) => + All(AssertHelper.ToEnumerable(collection), action); + + /// + /// Verifies that all items in the collection pass when executed against + /// action. The item index is provided to the action, in addition to the item. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static void All( + IAsyncEnumerable collection, + Action action) => + All(AssertHelper.ToEnumerable(collection), action); + + /// + /// Verifies that all items in the collection pass when executed against + /// action. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static Task AllAsync( + IAsyncEnumerable collection, + Func action) => + AllAsync(AssertHelper.ToEnumerable(collection), action); + + /// + /// Verifies that all items in the collection pass when executed against + /// action. The item index is provided to the action, in addition to the item. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static Task AllAsync( + IAsyncEnumerable collection, + Func action) => + AllAsync(AssertHelper.ToEnumerable(collection), action); + + /// + /// Verifies that a collection contains exactly a given number of elements, which meet + /// the criteria provided by the element inspectors. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The element inspectors, which inspect each element in turn. The + /// total number of element inspectors must exactly match the number of elements in the collection. + public static void Collection( + IAsyncEnumerable collection, + params Action[] elementInspectors) => + Collection(AssertHelper.ToEnumerable(collection), elementInspectors); + + /// + /// Verifies that a collection contains exactly a given number of elements, which meet + /// the criteria provided by the element inspectors. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The element inspectors, which inspect each element in turn. The + /// total number of element inspectors must exactly match the number of elements in the collection. + public static Task CollectionAsync( + IAsyncEnumerable collection, + params Func[] elementInspectors) => + CollectionAsync(AssertHelper.ToEnumerable(collection), elementInspectors); + + /// + /// Verifies that a collection contains a given object. + /// + /// The type of the object to be verified + /// The object expected to be in the collection + /// The collection to be inspected + /// Thrown when the object is not present in the collection + public static void Contains( + T expected, + IAsyncEnumerable collection) => + Contains(expected, AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that a collection contains a given object, using an equality comparer. + /// + /// The type of the object to be verified + /// The object expected to be in the collection + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when the object is not present in the collection + public static void Contains( + T expected, + IAsyncEnumerable collection, + IEqualityComparer comparer) => + Contains(expected, AssertHelper.ToEnumerable(collection), comparer); + + /// + /// Verifies that a collection contains a given object. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The filter used to find the item you're ensuring the collection contains + /// Thrown when the object is not present in the collection + public static void Contains( + IAsyncEnumerable collection, + Predicate filter) => + Contains(AssertHelper.ToEnumerable(collection), filter); + + /// + /// Verifies that a collection contains each object only once. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// Thrown when an object is present inside the collection more than once + public static void Distinct(IAsyncEnumerable collection) => + Distinct(AssertHelper.ToEnumerable(collection), EqualityComparer.Default); + + /// + /// Verifies that a collection contains each object only once. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when an object is present inside the collection more than once + public static void Distinct( + IAsyncEnumerable collection, + IEqualityComparer comparer) => + Distinct(AssertHelper.ToEnumerable(collection), comparer); + + /// + /// Verifies that a collection does not contain a given object. + /// + /// The type of the object to be compared + /// The object that is expected not to be in the collection + /// The collection to be inspected + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + T expected, + IAsyncEnumerable collection) => + DoesNotContain(expected, AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that a collection does not contain a given object, using an equality comparer. + /// + /// The type of the object to be compared + /// The object that is expected not to be in the collection + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + T expected, + IAsyncEnumerable collection, + IEqualityComparer comparer) => + DoesNotContain(expected, AssertHelper.ToEnumerable(collection), comparer); + + /// + /// Verifies that a collection does not contain a given object. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// The filter used to find the item you're ensuring the collection does not contain + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + IAsyncEnumerable collection, + Predicate filter) => + DoesNotContain(AssertHelper.ToEnumerable(collection), filter); + + /// + /// Verifies that a collection is empty. + /// + /// The collection to be inspected + /// Thrown when the collection is null + /// Thrown when the collection is not empty + public static void Empty(IAsyncEnumerable collection) => + Empty(AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that two sequences are equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual) => +#else + IEnumerable expected, + IAsyncEnumerable actual) => +#endif + Equal(expected, AssertHelper.ToEnumerable(actual), GetEqualityComparer()); + + /// + /// Verifies that two sequences are equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual) => +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual) => +#endif + Equal(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), GetEqualityComparer()); + + /// + /// Verifies that two sequences are equivalent, using a custom equatable comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The comparer used to compare the two objects + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual, +#else + IEnumerable expected, + IAsyncEnumerable actual, +#endif + IEqualityComparer comparer) => + Equal(expected, AssertHelper.ToEnumerable(actual), GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two sequences are equivalent, using a custom equatable comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The comparer used to compare the two objects + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual, +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual, +#endif + IEqualityComparer comparer) => + Equal(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two collections are equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual, +#else + IEnumerable expected, + IAsyncEnumerable actual, +#endif + Func comparer) => + Equal(expected, AssertHelper.ToEnumerable(actual), AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that two collections are equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void Equal( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual, +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual, +#endif + Func comparer) => + Equal(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that a collection is not empty. + /// + /// The collection to be inspected + /// Thrown when a null collection is passed + /// Thrown when the collection is empty + public static void NotEmpty(IAsyncEnumerable collection) => + NotEmpty(AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that two sequences are not equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual) => +#else + IEnumerable expected, + IAsyncEnumerable actual) => +#endif + NotEqual(expected, AssertHelper.ToEnumerable(actual), GetEqualityComparer()); + + /// + /// Verifies that two sequences are not equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual) => +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual) => +#endif + NotEqual(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), GetEqualityComparer()); + + /// + /// Verifies that two sequences are not equivalent, using a custom equality comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// The comparer used to compare the two objects + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual, +#else + IEnumerable expected, + IAsyncEnumerable actual, +#endif + IEqualityComparer comparer) => + NotEqual(expected, AssertHelper.ToEnumerable(actual), GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two sequences are not equivalent, using a custom equality comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// The comparer used to compare the two objects + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual, +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual, +#endif + IEqualityComparer comparer) => + NotEqual(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two collections are not equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual, +#else + IEnumerable expected, + IAsyncEnumerable actual, +#endif + Func comparer) => + NotEqual(expected, AssertHelper.ToEnumerable(actual), AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that two collections are not equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void NotEqual( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual, +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual, +#endif + Func comparer) => + NotEqual(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that the given collection contains only a single + /// element of the given type. + /// + /// The collection type. + /// The collection. + /// The single item in the collection. + /// Thrown when the collection does not contain + /// exactly one element. + public static T Single(IAsyncEnumerable collection) => + Single(AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that the given collection contains only a single + /// element of the given type which matches the given predicate. The + /// collection may or may not contain other values which do not + /// match the given predicate. + /// + /// The collection type. + /// The collection. + /// The item matching predicate. + /// The single item in the filtered collection. + /// Thrown when the filtered collection does + /// not contain exactly one element. + public static T Single( + IAsyncEnumerable collection, + Predicate predicate) => + Single(AssertHelper.ToEnumerable(collection), predicate); + } +} + +#endif diff --git a/src/Microsoft.DotNet.XUnitAssert/src/BooleanAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/BooleanAsserts.cs new file mode 100644 index 00000000000..4c2a4c02308 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/BooleanAsserts.cs @@ -0,0 +1,138 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8625 +#endif + +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// Thrown if the condition is not false +#if XUNIT_NULLABLE + public static void False([DoesNotReturnIf(parameterValue: true)] bool condition) => +#else + public static void False(bool condition) => +#endif + False((bool?)condition, null); + + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// Thrown if the condition is not false +#if XUNIT_NULLABLE + public static void False([DoesNotReturnIf(parameterValue: true)] bool? condition) => +#else + public static void False(bool? condition) => +#endif + False(condition, null); + + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// The message to show when the condition is not false + /// Thrown if the condition is not false + public static void False( +#if XUNIT_NULLABLE + [DoesNotReturnIf(parameterValue: true)] bool condition, + string? userMessage) => +#else + bool condition, + string userMessage) => +#endif + False((bool?)condition, userMessage); + + /// + /// Verifies that the condition is false. + /// + /// The condition to be tested + /// The message to show when the condition is not false + /// Thrown if the condition is not false + public static void False( +#if XUNIT_NULLABLE + [DoesNotReturnIf(parameterValue: true)] bool? condition, + string? userMessage) +#else + bool? condition, + string userMessage) +#endif + { + if (!condition.HasValue || condition.GetValueOrDefault()) + throw FalseException.ForNonFalseValue(userMessage, condition); + } + + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// Thrown when the condition is false +#if XUNIT_NULLABLE + public static void True([DoesNotReturnIf(parameterValue: false)] bool condition) => +#else + public static void True(bool condition) => +#endif + True((bool?)condition, null); + + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// Thrown when the condition is false +#if XUNIT_NULLABLE + public static void True([DoesNotReturnIf(parameterValue: false)] bool? condition) => +#else + public static void True(bool? condition) => +#endif + True(condition, null); + + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// The message to be shown when the condition is false + /// Thrown when the condition is false + public static void True( +#if XUNIT_NULLABLE + [DoesNotReturnIf(parameterValue: false)] bool condition, + string? userMessage) => +#else + bool condition, + string userMessage) => +#endif + True((bool?)condition, userMessage); + + /// + /// Verifies that an expression is true. + /// + /// The condition to be inspected + /// The message to be shown when the condition is false + /// Thrown when the condition is false + public static void True( +#if XUNIT_NULLABLE + [DoesNotReturnIf(parameterValue: false)] bool? condition, + string? userMessage) +#else + bool? condition, + string userMessage) +#endif + { + if (!condition.HasValue || !condition.GetValueOrDefault()) + throw TrueException.ForNonTrueValue(userMessage, condition); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/CollectionAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/CollectionAsserts.cs new file mode 100644 index 00000000000..331a9a5ea01 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/CollectionAsserts.cs @@ -0,0 +1,800 @@ +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA1720 // Identifier contains type name +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task +#pragma warning disable IDE0063 // Use simple 'using' statement +#pragma warning disable IDE0066 // Convert switch statement to expression +#pragma warning disable IDE0305 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#pragma warning disable CS8625 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that all items in the collection pass when executed against + /// action. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static void All( + IEnumerable collection, + Action action) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + All(collection, (item, index) => action(item), throwIfEmpty: false); + } + + /// + /// Verifies that all items in the collection pass when executed against + /// action. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Indicates whether to throw an exception if the collection is empty + /// Thrown when the collection contains at least one non-matching element + /// Also thrown when collection is empty and is set to true + public static void All( + IEnumerable collection, + Action action, + bool throwIfEmpty) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + All(collection, (item, index) => action(item), throwIfEmpty); + } + + /// + /// Verifies that all items in the collection pass when executed against + /// action. The item index is provided to the action, in addition to the item. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static void All( + IEnumerable collection, + Action action) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + All(collection, action, throwIfEmpty: false); + } + + /// + /// Verifies that all items in the collection pass when executed against + /// action. The item index is provided to the action, in addition to the item. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Indicates whether to throw an exception if the collection is empty + /// Thrown when the collection contains at least one non-matching element + /// Also thrown when collection is empty and is set to true + public static void All( + IEnumerable collection, + Action action, + bool throwIfEmpty) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + var errors = new List>(); + var idx = 0; + + foreach (var item in collection) + { + try + { + action(item, idx); + } + catch (Exception ex) + { + errors.Add(new Tuple(idx, ArgumentFormatter.Format(item), ex)); + } + + ++idx; + } + + if (throwIfEmpty && idx == 0) + throw AllException.ForEmptyCollection(); + + if (errors.Count > 0) + throw AllException.ForFailures(idx, errors); + } + + /// + /// Verifies that all items in the collection pass when executed against + /// action. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static async Task AllAsync( + IEnumerable collection, + Func action) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + await AllAsync(collection, async (item, index) => await action(item), throwIfEmpty: false); + } + + /// + /// Verifies that all items in the collection pass when executed against + /// action. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Indicates whether to throw an exception if the collection is empty + /// Thrown when the collection contains at least one non-matching element + /// Also thrown when collection is empty and is set to true + public static async Task AllAsync( + IEnumerable collection, + Func action, + bool throwIfEmpty) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + await AllAsync(collection, (item, index) => action(item), throwIfEmpty); + } + + /// + /// Verifies that all items in the collection pass when executed against + /// action. The item index is provided to the action, in addition to the item. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static async Task AllAsync( + IEnumerable collection, + Func action) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + await AllAsync(collection, action, throwIfEmpty: false); + } + + /// + /// Verifies that all items in the collection pass when executed against + /// action. The item index is provided to the action, in addition to the item. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Indicates whether to throw an exception if the collection is empty + /// Thrown when the collection contains at least one non-matching element + /// Also thrown when collection is empty and is set to true + public static async Task AllAsync( + IEnumerable collection, + Func action, + bool throwIfEmpty) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + var errors = new List>(); + var idx = 0; + + foreach (var item in collection) + { + try + { + await action(item, idx); + } + catch (Exception ex) + { + errors.Add(new Tuple(idx, ArgumentFormatter.Format(item), ex)); + } + + ++idx; + } + + if (throwIfEmpty && idx == 0) + throw AllException.ForEmptyCollection(); + + if (errors.Count > 0) + throw AllException.ForFailures(idx, errors.ToArray()); + } + + /// + /// Verifies that a collection contains exactly a given number of elements, which meet + /// the criteria provided by the element inspectors. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The element inspectors, which inspect each element in turn. The + /// total number of element inspectors must exactly match the number of elements in the collection. + public static void Collection( + IEnumerable collection, + params Action[] elementInspectors) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(elementInspectors), elementInspectors); + + using (var tracker = collection.AsTracker()) + { + var index = 0; + + foreach (var item in tracker) + { + try + { + if (index < elementInspectors.Length) + elementInspectors[index](item); + } + catch (Exception ex) + { + var formattedCollection = tracker.FormatIndexedMismatch(index, out var pointerIndent); + throw CollectionException.ForMismatchedItem(ex, index, pointerIndent, formattedCollection); + } + + index++; + } + + if (tracker.IterationCount != elementInspectors.Length) + throw CollectionException.ForMismatchedItemCount(elementInspectors.Length, tracker.IterationCount, tracker.FormatStart()); + } + } + + /// + /// Verifies that a collection contains exactly a given number of elements, which meet + /// the criteria provided by the element inspectors. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The element inspectors, which inspect each element in turn. The + /// total number of element inspectors must exactly match the number of elements in the collection. + public static async Task CollectionAsync( + IEnumerable collection, + params Func[] elementInspectors) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(elementInspectors), elementInspectors); + + using (var tracker = collection.AsTracker()) + { + var index = 0; + + foreach (var item in tracker) + { + try + { + if (index < elementInspectors.Length) + await elementInspectors[index](item); + } + catch (Exception ex) + { + var formattedCollection = tracker.FormatIndexedMismatch(index, out var pointerIndent); + throw CollectionException.ForMismatchedItem(ex, index, pointerIndent, formattedCollection); + } + + index++; + } + + if (tracker.IterationCount != elementInspectors.Length) + throw CollectionException.ForMismatchedItemCount(elementInspectors.Length, tracker.IterationCount, tracker.FormatStart()); + } + } + + /// + /// Verifies that a collection contains a given object. + /// + /// The type of the object to be verified + /// The object expected to be in the collection + /// The collection to be inspected + /// Thrown when the object is not present in the collection + public static void Contains( + T expected, + IEnumerable collection) + { + GuardArgumentNotNull(nameof(collection), collection); + + // We special case sets because they are constructed with their comparers, which we don't have access to. + // We want to let them do their normal logic when appropriate, and not try to use our default comparer. + if (collection is ISet set) + { + Contains(expected, set); + return; + } +#if NET8_0_OR_GREATER + if (collection is IReadOnlySet readOnlySet) + { + Contains(expected, readOnlySet); + return; + } +#endif + + // Fall back to the assumption that this is a linear container and use our default comparer + Contains(expected, collection, GetEqualityComparer()); + } + + /// + /// Verifies that a collection contains a given object, using an equality comparer. + /// + /// The type of the object to be verified + /// The object expected to be in the collection + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when the object is not present in the collection + public static void Contains( + T expected, + IEnumerable collection, + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(comparer), comparer); + + using (var tracker = collection.AsTracker()) + if (!tracker.Contains(expected, comparer)) + throw ContainsException.ForCollectionItemNotFound(ArgumentFormatter.Format(expected), tracker.FormatStart()); + } + + /// + /// Verifies that a collection contains a given object. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The filter used to find the item you're ensuring the collection contains + /// Thrown when the object is not present in the collection + public static void Contains( + IEnumerable collection, + Predicate filter) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(filter), filter); + + using (var tracker = collection.AsTracker()) + { + foreach (var item in tracker) + if (filter(item)) + return; + + throw ContainsException.ForCollectionFilterNotMatched(tracker.FormatStart()); + } + } + + /// + /// Verifies that a collection contains each object only once. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// Thrown when an object is present inside the collection more than once + public static void Distinct(IEnumerable collection) => + Distinct(collection, EqualityComparer.Default); + + /// + /// Verifies that a collection contains each object only once. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when an object is present inside the collection more than once + public static void Distinct( + IEnumerable collection, + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(comparer), comparer); + + using (var tracker = collection.AsTracker()) + { + var set = new HashSet(comparer); + + foreach (var item in tracker) + if (!set.Add(item)) + throw DistinctException.ForDuplicateItem(ArgumentFormatter.Format(item), tracker.FormatStart()); + } + } + + /// + /// Verifies that a collection does not contain a given object. + /// + /// The type of the object to be compared + /// The object that is expected not to be in the collection + /// The collection to be inspected + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + T expected, + IEnumerable collection) + { + GuardArgumentNotNull(nameof(collection), collection); + + // We special case sets because they are constructed with their comparers, which we don't have access to. + // We want to let them do their normal logic when appropriate, and not try to use our default comparer. + if (collection is ISet set) + { + DoesNotContain(expected, set); + return; + } +#if NET8_0_OR_GREATER + if (collection is IReadOnlySet readOnlySet) + { + DoesNotContain(expected, readOnlySet); + return; + } +#endif + + // Fall back to the assumption that this is a linear container and use our default comparer + DoesNotContain(expected, collection, GetEqualityComparer()); + } + + /// + /// Verifies that a collection does not contain a given object, using an equality comparer. + /// + /// The type of the object to be compared + /// The object that is expected not to be in the collection + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + T expected, + IEnumerable collection, + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(comparer), comparer); + + using (var tracker = collection.AsTracker()) + { + var index = 0; + + foreach (var item in tracker) + { + if (comparer.Equals(item, expected)) + { + var formattedCollection = tracker.FormatIndexedMismatch(index, out var pointerIndent); + + throw DoesNotContainException.ForCollectionItemFound( + ArgumentFormatter.Format(expected), + index, + pointerIndent, + formattedCollection + ); + } + + ++index; + } + } + } + + /// + /// Verifies that a collection does not contain a given object. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// The filter used to find the item you're ensuring the collection does not contain + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + IEnumerable collection, + Predicate filter) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(filter), filter); + + using (var tracker = collection.AsTracker()) + { + var index = 0; + + foreach (var item in tracker) + { + if (filter(item)) + { + var formattedCollection = tracker.FormatIndexedMismatch(index, out var pointerIndent); + + throw DoesNotContainException.ForCollectionFilterMatched( + index, + pointerIndent, + formattedCollection + ); + } + + ++index; + } + } + } + + /// + /// Verifies that a collection is empty. + /// + /// The collection to be inspected + /// Thrown when the collection is null + /// Thrown when the collection is not empty + public static void Empty(IEnumerable collection) + { + GuardArgumentNotNull(nameof(collection), collection); + + using (var tracker = collection.AsTracker()) + { + var enumerator = tracker.GetEnumerator(); + if (enumerator.MoveNext()) + throw EmptyException.ForNonEmptyCollection(tracker.FormatStart()); + } + } + + /// + /// Verifies that two sequences are equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IEnumerable? actual) => +#else + IEnumerable expected, + IEnumerable actual) => +#endif + Equal(expected, actual, GetEqualityComparer>()); + + /// + /// Verifies that two sequences are equivalent, using a custom equatable comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The comparer used to compare the two objects + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IEnumerable? actual, +#else + IEnumerable expected, + IEnumerable actual, +#endif + IEqualityComparer comparer) => + Equal(expected, actual, GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two collections are equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IEnumerable? actual, +#else + IEnumerable expected, + IEnumerable actual, +#endif + Func comparer) => + Equal(expected, actual, AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that a collection is not empty. + /// + /// The collection to be inspected + /// Thrown when a null collection is passed + /// Thrown when the collection is empty + public static void NotEmpty(IEnumerable collection) + { + GuardArgumentNotNull(nameof(collection), collection); + + var enumerator = collection.GetEnumerator(); + try + { + if (!enumerator.MoveNext()) + throw NotEmptyException.ForNonEmptyCollection(); + } + finally + { + (enumerator as IDisposable)?.Dispose(); + } + } + + /// + /// Verifies that two sequences are not equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IEnumerable? actual) => +#else + IEnumerable expected, + IEnumerable actual) => +#endif + NotEqual(expected, actual, GetEqualityComparer>()); + + /// + /// Verifies that two sequences are not equivalent, using a custom equality comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// The comparer used to compare the two objects + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IEnumerable? actual, +#else + IEnumerable expected, + IEnumerable actual, +#endif + IEqualityComparer comparer) => + NotEqual(expected, actual, GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two collections are not equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IEnumerable? actual, +#else + IEnumerable expected, + IEnumerable actual, +#endif + Func comparer) => + NotEqual(expected, actual, AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that the given collection contains only a single + /// element of the given type. + /// + /// The collection. + /// The single item in the collection. + /// Thrown when the collection does not contain + /// exactly one element. +#if XUNIT_NULLABLE + public static object? Single(IEnumerable collection) +#else + public static object Single(IEnumerable collection) +#endif + { + GuardArgumentNotNull(nameof(collection), collection); + + return Single(collection.Cast()); + } + + /// + /// Verifies that the given collection contains only a single + /// element of the given value. The collection may or may not + /// contain other values. + /// + /// The collection. + /// The value to find in the collection. + /// The single item in the collection. + /// Thrown when the collection does not contain + /// exactly one element. + public static void Single( + IEnumerable collection, +#if XUNIT_NULLABLE + object? expected) +#else + object expected) +#endif + { + GuardArgumentNotNull(nameof(collection), collection); + + GetSingleResult(collection.Cast(), item => object.Equals(item, expected), ArgumentFormatter.Format(expected)); + } + + /// + /// Verifies that the given collection contains only a single + /// element of the given type. + /// + /// The collection type. + /// The collection. + /// The single item in the collection. + /// Thrown when the collection does not contain + /// exactly one element. + public static T Single(IEnumerable collection) + { + GuardArgumentNotNull(nameof(collection), collection); + + return GetSingleResult(collection, null, null); + } + + /// + /// Verifies that the given collection contains only a single + /// element of the given type which matches the given predicate. The + /// collection may or may not contain other values which do not + /// match the given predicate. + /// + /// The collection type. + /// The collection. + /// The item matching predicate. + /// The single item in the filtered collection. + /// Thrown when the filtered collection does + /// not contain exactly one element. + public static T Single( + IEnumerable collection, + Predicate predicate) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(predicate), predicate); + + return GetSingleResult(collection, predicate, "(predicate expression)"); + } + + static T GetSingleResult( + IEnumerable collection, +#if XUNIT_NULLABLE + Predicate? predicate, + string? expected) +#else + Predicate predicate, + string expected) +#endif + { + var count = 0; + var index = 0; + var matchIndices = new List(); + var result = default(T); + + using (var tracker = collection.AsTracker()) + { + foreach (var item in tracker) + { + if (predicate == null || predicate(item)) + { + if (++count == 1) + result = item; + if (predicate != null) + matchIndices.Add(index); + } + + ++index; + } + + switch (count) + { + case 0: + throw SingleException.Empty(expected, tracker.FormatStart()); + case 1: +#if XUNIT_NULLABLE + return result!; +#else + return result; +#endif + default: + throw SingleException.MoreThanOne(count, expected, tracker.FormatStart(), matchIndices); + } + } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Comparers.cs b/src/Microsoft.DotNet.XUnitAssert/src/Comparers.cs new file mode 100644 index 00000000000..90ac761d2bc --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Comparers.cs @@ -0,0 +1,32 @@ +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA1859 // Use concrete types when possible for improved performance + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8625 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { +#if XUNIT_NULLABLE + static IEqualityComparer GetEqualityComparer(IEqualityComparer? innerComparer = null) => + new AssertEqualityComparer(innerComparer); +#else + static IEqualityComparer GetEqualityComparer(IEqualityComparer innerComparer = null) => + new AssertEqualityComparer(innerComparer); +#endif + + static IComparer GetRangeComparer() + where T : IComparable => + new AssertRangeComparer(); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/DictionaryAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/DictionaryAsserts.cs new file mode 100644 index 00000000000..41b71f2829a --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/DictionaryAsserts.cs @@ -0,0 +1,261 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#pragma warning disable CS8714 +#endif + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that a dictionary contains a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// The value associated with . + /// Thrown when the object is not present in the collection + public static TValue Contains( + TKey expected, + IDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + { + GuardArgumentNotNull(nameof(expected), expected); + GuardArgumentNotNull(nameof(collection), collection); + + if (!collection.TryGetValue(expected, out var value)) + throw ContainsException.ForKeyNotFound( + ArgumentFormatter.Format(expected), + CollectionTracker.FormatStart(collection.Keys) + ); + + return value; + } + + /// + /// Verifies that a read-only dictionary contains a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// The value associated with . + /// Thrown when the object is not present in the collection + public static TValue Contains( + TKey expected, + IReadOnlyDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + { + GuardArgumentNotNull(nameof(expected), expected); + GuardArgumentNotNull(nameof(collection), collection); + + if (!collection.TryGetValue(expected, out var value)) + throw ContainsException.ForKeyNotFound( + ArgumentFormatter.Format(expected), + CollectionTracker.FormatStart(collection.Keys) + ); + + return value; + } + + /// + /// Verifies that a dictionary contains a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// The value associated with . + /// Thrown when the object is not present in the collection + public static TValue Contains( + TKey expected, + ConcurrentDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + => Contains(expected, (IReadOnlyDictionary)collection); + + /// + /// Verifies that a dictionary contains a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// The value associated with . + /// Thrown when the object is not present in the collection + public static TValue Contains( + TKey expected, + Dictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + => Contains(expected, (IReadOnlyDictionary)collection); + + /// + /// Verifies that a dictionary contains a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// The value associated with . + /// Thrown when the object is not present in the collection + public static TValue Contains( + TKey expected, + ReadOnlyDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + => Contains(expected, (IReadOnlyDictionary)collection); + + /// + /// Verifies that a dictionary contains a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// The value associated with . + /// Thrown when the object is not present in the collection + public static TValue Contains( + TKey expected, + ImmutableDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + => Contains(expected, (IReadOnlyDictionary)collection); + + /// + /// Verifies that a dictionary does not contain a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// Thrown when the object is present in the collection + public static void DoesNotContain( + TKey expected, + IDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + { + GuardArgumentNotNull(nameof(expected), expected); + GuardArgumentNotNull(nameof(collection), collection); + + // Do not forward to DoesNotContain(expected, collection.Keys) as we want the default SDK behavior + if (collection.ContainsKey(expected)) + throw DoesNotContainException.ForKeyFound( + ArgumentFormatter.Format(expected), + CollectionTracker.FormatStart(collection.Keys) + ); + } + + /// + /// Verifies that a dictionary does not contain a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// Thrown when the object is present in the collection + public static void DoesNotContain( + TKey expected, + IReadOnlyDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + { + GuardArgumentNotNull(nameof(expected), expected); + GuardArgumentNotNull(nameof(collection), collection); + + // Do not forward to DoesNotContain(expected, collection.Keys) as we want the default SDK behavior + if (collection.ContainsKey(expected)) + throw DoesNotContainException.ForKeyFound( + ArgumentFormatter.Format(expected), + CollectionTracker.FormatStart(collection.Keys) + ); + } + + /// + /// Verifies that a dictionary does not contain a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// Thrown when the object is present in the collection + public static void DoesNotContain( + TKey expected, + ConcurrentDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + => DoesNotContain(expected, (IReadOnlyDictionary)collection); + + /// + /// Verifies that a dictionary does not contain a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// Thrown when the object is present in the collection + public static void DoesNotContain( + TKey expected, + Dictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + => DoesNotContain(expected, (IReadOnlyDictionary)collection); + + /// + /// Verifies that a dictionary does not contain a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// Thrown when the object is present in the collection + public static void DoesNotContain( + TKey expected, + ReadOnlyDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + => DoesNotContain(expected, (IReadOnlyDictionary)collection); + + /// + /// Verifies that a dictionary does not contain a given key. + /// + /// The type of the keys of the object to be verified. + /// The type of the values of the object to be verified. + /// The object expected to be in the collection. + /// The collection to be inspected. + /// Thrown when the object is present in the collection + public static void DoesNotContain( + TKey expected, + ImmutableDictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + => DoesNotContain(expected, (IReadOnlyDictionary)collection); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts.cs new file mode 100644 index 00000000000..be9cc27044a --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts.cs @@ -0,0 +1,845 @@ +#pragma warning disable CA1031 // Do not catch general exception types + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8600 +#pragma warning disable CS8604 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using Xunit.Internal; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that two objects are equal, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + public static void Equal( +#if XUNIT_NULLABLE + T? expected, + T? actual) => +#else + T expected, + T actual) => +#endif + Equal(expected, actual, GetEqualityComparer()); + + /// + /// Verifies that two objects are equal, using a custom comparer function. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The comparer used to compare the two objects + public static void Equal( +#if XUNIT_NULLABLE + T? expected, + T? actual, +#else + T expected, + T actual, +#endif + Func comparer) => + Equal(expected, actual, AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that two objects are equal, using a custom equality comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The comparer used to compare the two objects + public static void Equal( +#if XUNIT_NULLABLE + T? expected, + T? actual, +#else + T expected, + T actual, +#endif + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(comparer), comparer); + + if (expected == null && actual == null) + return; + + var expectedTracker = expected.AsNonStringTracker(); + var actualTracker = actual.AsNonStringTracker(); + var exception = default(Exception); + var mismatchedIndex = default(int?); + + try + { + var haveCollections = + (expectedTracker != null && actualTracker != null) || + (expectedTracker != null && actual == null) || + (expected == null && actualTracker != null); + + if (!haveCollections) + { + try + { + if (comparer.Equals(expected, actual)) + return; + } + catch (Exception ex) + { + exception = ex; + } + + throw EqualException.ForMismatchedValuesWithError( + expected as string ?? ArgumentFormatter.Format(expected), + actual as string ?? ArgumentFormatter.Format(actual), + exception + ); + } + else + { + // If we have "known" comparers, we can ignore them and instead do our own thing, since we know + // we want to be able to consume the tracker, and that's not type compatible. + var itemComparer = default(IEqualityComparer); + + var aec = comparer as AssertEqualityComparer; + if (aec != null) + itemComparer = aec.InnerComparer; + else if (comparer == EqualityComparer.Default) + itemComparer = EqualityComparer.Default; + + string formattedExpected; + string formattedActual; + int? expectedPointer = null; + int? actualPointer = null; +#if XUNIT_NULLABLE + string? expectedItemType = null; + string? actualItemType = null; +#else + string expectedItemType = null; + string actualItemType = null; +#endif + + if (itemComparer != null) + { + AssertEqualityResult result; + + // Call AssertEqualityComparer.Equals because it checks for IEquatable<> before using CollectionTracker + if (aec != null) + result = aec.Equals(expected, expectedTracker, actual, actualTracker); + else + result = CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer); + + if (result.Equal) + return; + + if (result.InnerResult is AssertEqualityResult innerResult) + { + var innerExpectedString = innerResult.X as string; + var innerExpectedMismatch = innerResult.MismatchIndexX; + var innerActualString = innerResult.Y as string; + var innerActualMismatch = innerResult.MismatchIndexY; + + if ((innerExpectedString != null || innerActualString != null) && innerExpectedMismatch.HasValue && innerActualMismatch.HasValue) + throw EqualException.ForMismatchedStringsWithHeader( + innerExpectedString, + innerActualString, + innerExpectedMismatch.Value, + innerActualMismatch.Value, + "Collections differ at index " + result.MismatchIndexX + ); + } + + exception = result.Exception; + mismatchedIndex = result.MismatchIndexX; + + var expectedStartIdx = -1; + var expectedEndIdx = -1; + expectedTracker?.GetMismatchExtents(mismatchedIndex, out expectedStartIdx, out expectedEndIdx); + + var actualStartIdx = -1; + var actualEndIdx = -1; + actualTracker?.GetMismatchExtents(mismatchedIndex, out actualStartIdx, out actualEndIdx); + + // If either located index is past the end of the collection, then we want to try to shift + // the too-short collection start point forward so we can align the equal values for + // a more readable and obvious output. See CollectionAssertTests+Equals+Arrays.Truncation + // for overrun examples. + if (mismatchedIndex.HasValue) + { + if (expectedStartIdx > -1 && expectedEndIdx < mismatchedIndex.Value) + expectedStartIdx = actualStartIdx; + else if (actualStartIdx > -1 && actualEndIdx < mismatchedIndex.Value) + actualStartIdx = expectedStartIdx; + } + + expectedPointer = null; + formattedExpected = expectedTracker?.FormatIndexedMismatch(expectedStartIdx, expectedEndIdx, mismatchedIndex, out expectedPointer) ?? ArgumentFormatter.Format(expected); + expectedItemType = expectedTracker?.TypeAt(mismatchedIndex); + + actualPointer = null; + formattedActual = actualTracker?.FormatIndexedMismatch(actualStartIdx, actualEndIdx, mismatchedIndex, out actualPointer) ?? ArgumentFormatter.Format(actual); + actualItemType = actualTracker?.TypeAt(mismatchedIndex); + } + else + { + try + { + if (comparer.Equals(expected, actual)) + return; + } + catch (Exception ex) + { + exception = ex; + } + + formattedExpected = ArgumentFormatter.Format(expected); + formattedActual = ArgumentFormatter.Format(actual); + } + + var expectedType = expected?.GetType(); + var expectedTypeDefinition = SafeGetGenericTypeDefinition(expectedType); + + var actualType = actual?.GetType(); + var actualTypeDefinition = SafeGetGenericTypeDefinition(actualType); + + var collectionDisplay = GetCollectionDisplay(expectedType, expectedTypeDefinition, actualType, actualTypeDefinition); + + if (expectedType != actualType) + { + var expectedTypeName = expectedType == null ? "" : (AssertHelper.IsCompilerGenerated(expectedType) ? " " : ArgumentFormatter.FormatTypeName(expectedType) + " "); + var actualTypeName = actualType == null ? "" : (AssertHelper.IsCompilerGenerated(actualType) ? " " : ArgumentFormatter.FormatTypeName(actualType) + " "); + + var typeNameIndent = Math.Max(expectedTypeName.Length, actualTypeName.Length); + + formattedExpected = expectedTypeName.PadRight(typeNameIndent) + formattedExpected; + formattedActual = actualTypeName.PadRight(typeNameIndent) + formattedActual; + + if (expectedPointer != null) + expectedPointer += typeNameIndent; + if (actualPointer != null) + actualPointer += typeNameIndent; + } + + throw EqualException.ForMismatchedCollectionsWithError(mismatchedIndex, formattedExpected, expectedPointer, expectedItemType, formattedActual, actualPointer, actualItemType, exception, collectionDisplay); + } + } + finally + { + expectedTracker?.Dispose(); + actualTracker?.Dispose(); + } + } + + /// + /// Verifies that two values are equal, within the number of decimal + /// places given by . The values are rounded before comparison. + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-15) + public static void Equal( + double expected, + double actual, + int precision) + { + var expectedRounded = Math.Round(expected, precision); + var actualRounded = Math.Round(actual, precision); + + if (!object.Equals(expectedRounded, actualRounded)) + throw EqualException.ForMismatchedValues( + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are not within {0} decimal place{1}", precision, precision == 1 ? "" : "s") + ); + } + + /// + /// Verifies that two values are equal, within the number of decimal + /// places given by . The values are rounded before comparison. + /// The rounding method to use is given by + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-15) + /// Rounding method to use to process a number that is midway between two numbers + public static void Equal( + double expected, + double actual, + int precision, + MidpointRounding rounding) + { + var expectedRounded = Math.Round(expected, precision, rounding); + var actualRounded = Math.Round(actual, precision, rounding); + + if (!object.Equals(expectedRounded, actualRounded)) + throw EqualException.ForMismatchedValues( + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are not within {0} decimal place{1}", precision, precision == 1 ? "" : "s") + ); + } + + /// + /// Verifies that two values are equal, within the tolerance given by + /// (positive or negative). + /// + /// The expected value + /// The value to be compared against + /// The allowed difference between values + public static void Equal( + double expected, + double actual, + double tolerance) + { + if (double.IsNaN(tolerance) || double.IsNegativeInfinity(tolerance) || tolerance < 0.0) + throw new ArgumentException("Tolerance must be greater than or equal to zero", nameof(tolerance)); + + if (!(object.Equals(expected, actual) || Math.Abs(expected - actual) <= tolerance)) + throw EqualException.ForMismatchedValues( + expected.ToString("G17", CultureInfo.CurrentCulture), + actual.ToString("G17", CultureInfo.CurrentCulture), + string.Format(CultureInfo.CurrentCulture, "Values are not within tolerance {0:G17}", tolerance) + ); + } + + /// + /// Verifies that two values are equal, within the number of decimal + /// places given by . The values are rounded before comparison. + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-15) + public static void Equal( + float expected, + float actual, + int precision) + { + var expectedRounded = Math.Round(expected, precision); + var actualRounded = Math.Round(actual, precision); + + if (!object.Equals(expectedRounded, actualRounded)) + throw EqualException.ForMismatchedValues( + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are not within {0} decimal place{1}", precision, precision == 1 ? "" : "s") + ); + } + + /// + /// Verifies that two values are equal, within the number of decimal + /// places given by . The values are rounded before comparison. + /// The rounding method to use is given by + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-15) + /// Rounding method to use to process a number that is midway between two numbers + public static void Equal( + float expected, + float actual, + int precision, + MidpointRounding rounding) + { + var expectedRounded = Math.Round(expected, precision, rounding); + var actualRounded = Math.Round(actual, precision, rounding); + + if (!object.Equals(expectedRounded, actualRounded)) + throw EqualException.ForMismatchedValues( + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are not within {0} decimal place{1}", precision, precision == 1 ? "" : "s") + ); + } + + /// + /// Verifies that two values are equal, within the tolerance given by + /// (positive or negative). + /// + /// The expected value + /// The value to be compared against + /// The allowed difference between values + public static void Equal( + float expected, + float actual, + float tolerance) + { + if (float.IsNaN(tolerance) || float.IsNegativeInfinity(tolerance) || tolerance < 0.0) + throw new ArgumentException("Tolerance must be greater than or equal to zero", nameof(tolerance)); + + if (!(object.Equals(expected, actual) || Math.Abs(expected - actual) <= tolerance)) + throw EqualException.ForMismatchedValues( + expected.ToString("G9", CultureInfo.CurrentCulture), + actual.ToString("G9", CultureInfo.CurrentCulture), + string.Format(CultureInfo.CurrentCulture, "Values are not within tolerance {0:G9}", tolerance) + ); + } + + /// + /// Verifies that two values are equal, within the number of decimal + /// places given by . The values are rounded before comparison. + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-28) + public static void Equal( + decimal expected, + decimal actual, + int precision) + { + var expectedRounded = Math.Round(expected, precision); + var actualRounded = Math.Round(actual, precision); + + if (expectedRounded != actualRounded) + throw EqualException.ForMismatchedValues( + string.Format(CultureInfo.CurrentCulture, "{0} (rounded from {1})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0} (rounded from {1})", actualRounded, actual) + ); + } + + /// + /// Verifies that two values are equal. + /// + /// The expected value + /// The value to be compared against + public static void Equal( + DateTime expected, + DateTime actual) => + Equal(expected, actual, TimeSpan.Zero); + + /// + /// Verifies that two values are equal, within the precision + /// given by . + /// + /// The expected value + /// The value to be compared against + /// The allowed difference in time where the two dates are considered equal + public static void Equal( + DateTime expected, + DateTime actual, + TimeSpan precision) + { + var difference = (expected - actual).Duration(); + + if (difference > precision) + { + var actualValue = + ArgumentFormatter.Format(actual) + + (precision == TimeSpan.Zero ? "" : string.Format(CultureInfo.CurrentCulture, " (difference {0} is larger than {1})", difference, precision)); + + throw EqualException.ForMismatchedValues(ArgumentFormatter.Format(expected), actualValue); + } + } + + /// + /// Verifies that two values are equal. + /// + /// The expected value + /// The value to be compared against + public static void Equal( + DateTimeOffset expected, + DateTimeOffset actual) => + Equal(expected, actual, TimeSpan.Zero); + + /// + /// Verifies that two values are equal, within the precision + /// given by . + /// + /// The expected value + /// The value to be compared against + /// The allowed difference in time where the two dates are considered equal + public static void Equal( + DateTimeOffset expected, + DateTimeOffset actual, + TimeSpan precision) + { + var difference = (expected - actual).Duration(); + + if (difference > precision) + { + var actualValue = + ArgumentFormatter.Format(actual) + + (precision == TimeSpan.Zero ? "" : string.Format(CultureInfo.CurrentCulture, " (difference {0} is larger than {1})", difference, precision)); + + throw EqualException.ForMismatchedValues(ArgumentFormatter.Format(expected), actualValue); + } + } + + /// + /// Verifies that two objects are not equal, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + public static void NotEqual( +#if XUNIT_NULLABLE + T? expected, + T? actual) => +#else + T expected, + T actual) => +#endif + NotEqual(expected, actual, GetEqualityComparer()); + + /// + /// Verifies that two objects are not equal, using a custom equality comparer function. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// The comparer used to examine the objects + public static void NotEqual( +#if XUNIT_NULLABLE + T? expected, + T? actual, +#else + T expected, + T actual, +#endif + Func comparer) => + NotEqual(expected, actual, AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that two objects are not equal, using a custom equality comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// The comparer used to examine the objects + public static void NotEqual( +#if XUNIT_NULLABLE + T? expected, + T? actual, +#else + T expected, + T actual, +#endif + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(comparer), comparer); + + var expectedTracker = expected.AsNonStringTracker(); + var actualTracker = actual.AsNonStringTracker(); + var exception = default(Exception); + var mismatchedIndex = default(int?); + + try + { + var haveCollections = + (expectedTracker != null && actualTracker != null) || + (expectedTracker != null && actual == null) || + (expected == null && actualTracker != null); + + if (!haveCollections) + { + try + { + if (!comparer.Equals(expected, actual)) + return; + } + catch (Exception ex) + { + exception = ex; + } + + var formattedExpected = ArgumentFormatter.Format(expected); + var formattedActual = ArgumentFormatter.Format(actual); + + var expectedIsString = expected is string; + var actualIsString = actual is string; + var isStrings = + (expectedIsString && actual == null) || + (actualIsString && expected == null) || + (expectedIsString && actualIsString); + + if (isStrings) + throw NotEqualException.ForEqualCollectionsWithError(null, formattedExpected, null, formattedActual, null, exception, "Strings"); + else + throw NotEqualException.ForEqualValuesWithError(formattedExpected, formattedActual, exception); + } + else + { + // If we have "known" comparers, we can ignore them and instead do our own thing, since we know + // we want to be able to consume the tracker, and that's not type compatible. + var itemComparer = default(IEqualityComparer); + + var aec = comparer as AssertEqualityComparer; + if (aec != null) + itemComparer = aec.InnerComparer; + else if (comparer == EqualityComparer.Default) + itemComparer = EqualityComparer.Default; + + string formattedExpected; + string formattedActual; + int? expectedPointer = null; + int? actualPointer = null; + + if (itemComparer != null) + { + AssertEqualityResult result; + + // Call AssertEqualityComparer.Equals because it checks for IEquatable<> before using CollectionTracker + if (aec != null) + result = aec.Equals(expected, expectedTracker, actual, actualTracker); + else + result = CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer); + + if (!result.Equal && result.Exception is null) + return; + + mismatchedIndex = result.MismatchIndexX; + + if (result.Exception is null) + { + // For NotEqual that doesn't throw, pointers are irrelevant, because + // the values are considered to be equal + formattedExpected = expectedTracker?.FormatStart() ?? "null"; + formattedActual = actualTracker?.FormatStart() ?? "null"; + } + else + { + exception = result.Exception; + + // When an exception was thrown, we want to provide a pointer so the user knows + // which item was being inspected when the exception was thrown + var expectedStartIdx = -1; + var expectedEndIdx = -1; + expectedTracker?.GetMismatchExtents(mismatchedIndex, out expectedStartIdx, out expectedEndIdx); + + var actualStartIdx = -1; + var actualEndIdx = -1; + actualTracker?.GetMismatchExtents(mismatchedIndex, out actualStartIdx, out actualEndIdx); + + expectedPointer = null; + formattedExpected = expectedTracker?.FormatIndexedMismatch(expectedStartIdx, expectedEndIdx, mismatchedIndex, out expectedPointer) ?? ArgumentFormatter.Format(expected); + + actualPointer = null; + formattedActual = actualTracker?.FormatIndexedMismatch(actualStartIdx, actualEndIdx, mismatchedIndex, out actualPointer) ?? ArgumentFormatter.Format(actual); + } + } + else + { + try + { + if (!comparer.Equals(expected, actual)) + return; + } + catch (Exception ex) + { + exception = ex; + } + + formattedExpected = ArgumentFormatter.Format(expected); + formattedActual = ArgumentFormatter.Format(actual); + } + + var expectedType = expected?.GetType(); + var expectedTypeDefinition = SafeGetGenericTypeDefinition(expectedType); + + var actualType = actual?.GetType(); + var actualTypeDefinition = SafeGetGenericTypeDefinition(actualType); + + var collectionDisplay = GetCollectionDisplay(expectedType, expectedTypeDefinition, actualType, actualTypeDefinition); + + if (expectedType != actualType) + { + var expectedTypeName = expectedType == null ? "" : (AssertHelper.IsCompilerGenerated(expectedType) ? " " : ArgumentFormatter.FormatTypeName(expectedType) + " "); + var actualTypeName = actualType == null ? "" : (AssertHelper.IsCompilerGenerated(actualType) ? " " : ArgumentFormatter.FormatTypeName(actualType) + " "); + + var typeNameIndent = Math.Max(expectedTypeName.Length, actualTypeName.Length); + + formattedExpected = expectedTypeName.PadRight(typeNameIndent) + formattedExpected; + formattedActual = actualTypeName.PadRight(typeNameIndent) + formattedActual; + + if (expectedPointer != null) + expectedPointer += typeNameIndent; + if (actualPointer != null) + actualPointer += typeNameIndent; + } + + throw NotEqualException.ForEqualCollectionsWithError(mismatchedIndex, formattedExpected, expectedPointer, formattedActual, actualPointer, exception, collectionDisplay); + } + } + finally + { + expectedTracker?.Dispose(); + actualTracker?.Dispose(); + } + } + + /// + /// Verifies that two values are not equal, within the number of decimal + /// places given by . + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-15) + public static void NotEqual( + double expected, + double actual, + int precision) + { + var expectedRounded = Math.Round(expected, precision); + var actualRounded = Math.Round(actual, precision); + + if (object.Equals(expectedRounded, actualRounded)) + throw NotEqualException.ForEqualValues( + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are within {0} decimal places", precision) + ); + } + + /// + /// Verifies that two values are not equal, within the number of decimal + /// places given by . The values are rounded before comparison. + /// The rounding method to use is given by + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-15) + /// Rounding method to use to process a number that is midway between two numbers + public static void NotEqual( + double expected, + double actual, + int precision, + MidpointRounding rounding) + { + var expectedRounded = Math.Round(expected, precision, rounding); + var actualRounded = Math.Round(actual, precision, rounding); + + if (object.Equals(expectedRounded, actualRounded)) + throw NotEqualException.ForEqualValues( + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are within {0} decimal places", precision) + ); + } + + /// + /// Verifies that two values are not equal, within the tolerance given by + /// (positive or negative). + /// + /// The expected value + /// The value to be compared against + /// The allowed difference between values + public static void NotEqual( + double expected, + double actual, + double tolerance) + { + if (double.IsNaN(tolerance) || double.IsNegativeInfinity(tolerance) || tolerance < 0.0) + throw new ArgumentException("Tolerance must be greater than or equal to zero", nameof(tolerance)); + + if (object.Equals(expected, actual) || Math.Abs(expected - actual) <= tolerance) + throw NotEqualException.ForEqualValues( + expected.ToString("G17", CultureInfo.CurrentCulture), + actual.ToString("G17", CultureInfo.CurrentCulture), + string.Format(CultureInfo.CurrentCulture, "Values are within tolerance {0:G17}", tolerance) + ); + } + + /// + /// Verifies that two values are not equal, within the number of decimal + /// places given by . + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-15) + public static void NotEqual( + float expected, + float actual, + int precision) + { + var expectedRounded = Math.Round(expected, precision); + var actualRounded = Math.Round(actual, precision); + + if (object.Equals(expectedRounded, actualRounded)) + throw NotEqualException.ForEqualValues( + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are within {0} decimal places", precision) + ); + } + + /// + /// Verifies that two values are not equal, within the number of decimal + /// places given by . The values are rounded before comparison. + /// The rounding method to use is given by + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-15) + /// Rounding method to use to process a number that is midway between two numbers + public static void NotEqual( + float expected, + float actual, + int precision, + MidpointRounding rounding) + { + var expectedRounded = Math.Round(expected, precision, rounding); + var actualRounded = Math.Round(actual, precision, rounding); + + if (object.Equals(expectedRounded, actualRounded)) + throw NotEqualException.ForEqualValues( + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are within {0} decimal places", precision) + ); + } + + /// + /// Verifies that two values are not equal, within the tolerance given by + /// (positive or negative). + /// + /// The expected value + /// The value to be compared against + /// The allowed difference between values + public static void NotEqual( + float expected, + float actual, + float tolerance) + { + if (float.IsNaN(tolerance) || float.IsNegativeInfinity(tolerance) || tolerance < 0.0) + throw new ArgumentException("Tolerance must be greater than or equal to zero", nameof(tolerance)); + + if (object.Equals(expected, actual) || Math.Abs(expected - actual) <= tolerance) + throw NotEqualException.ForEqualValues( + expected.ToString("G9", CultureInfo.CurrentCulture), + actual.ToString("G9", CultureInfo.CurrentCulture), + string.Format(CultureInfo.CurrentCulture, "Values are within tolerance {0:G9}", tolerance) + ); + } + + /// + /// Verifies that two values are not equal, within the number of decimal + /// places given by . + /// + /// The expected value + /// The value to be compared against + /// The number of decimal places (valid values: 0-28) + public static void NotEqual( + decimal expected, + decimal actual, + int precision) + { + var expectedRounded = Math.Round(expected, precision); + var actualRounded = Math.Round(actual, precision); + + if (expectedRounded == actualRounded) + throw NotEqualException.ForEqualValues( + string.Format(CultureInfo.CurrentCulture, "{0} (rounded from {1})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0} (rounded from {1})", actualRounded, actual) + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts_aot.cs b/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts_aot.cs new file mode 100644 index 00000000000..1a00ec54b1f --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts_aot.cs @@ -0,0 +1,167 @@ +#if XUNIT_AOT + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that two values in a are equal. + /// + /// The type of the key. + /// The type of the value. + /// The expected key/value pair + /// The actual key/value pair + [OverloadResolutionPriority(1)] + public static void Equal( + KeyValuePair? expected, + KeyValuePair? actual) + { + if (!expected.HasValue) + { + if (!actual.HasValue) + return; + + throw EqualException.ForMismatchedValues(ArgumentFormatter.Format(expected), ArgumentFormatter.Format(actual)); + } + + if (actual == null) + throw EqualException.ForMismatchedValues(ArgumentFormatter.Format(expected), ArgumentFormatter.Format(actual)); + + var keyComparer = new AssertEqualityComparer(); + if (!keyComparer.Equals(expected.Value.Key, actual.Value.Key)) + throw EqualException.ForMismatchedValues( + ArgumentFormatter.Format(expected.Value.Key), + ArgumentFormatter.Format(actual.Value.Key), + "Keys differ in KeyValuePair" + ); + + var valueComparer = new AssertEqualityComparer(); + if (!valueComparer.Equals(expected.Value.Value, actual.Value.Value)) + throw EqualException.ForMismatchedValues( + ArgumentFormatter.Format(expected.Value.Value), + ArgumentFormatter.Format(actual.Value.Value), + "Values differ in KeyValuePair" + ); + } + +#pragma warning disable IDE0060 // Remove unused parameter + +#if XUNIT_NULLABLE + static string? GetCollectionDisplay( + Type? expectedType, + Type? expectedTypeDefinition, + Type? actualType, + Type? actualTypeDefinition) +#else + static string? GetCollectionDisplay( + Type expectedType, + Type expectedTypeDefinition, + Type actualType, + Type actualTypeDefinition) +#endif + { + if (expectedTypeDefinition == typeofDictionary && actualTypeDefinition == typeofDictionary) + return "Dictionaries"; + else if (expectedTypeDefinition == typeofHashSet && actualTypeDefinition == typeofHashSet) + return "HashSets"; + + return null; + } + +#pragma warning restore IDE0060 // Remove unused parameter + + /// + /// Verifies that two values in a are not equal. + /// + /// The type of the key. + /// The type of the value. + /// The expected key/value pair + /// The actual key/value pair + [OverloadResolutionPriority(1)] + public static void NotEqual( + KeyValuePair? expected, + KeyValuePair? actual) + { + if (expected == null) + { + if (actual != null) + return; + + throw NotEqualException.ForEqualValues("null", "null"); + } + + if (actual == null) + return; + + var keyComparer = new AssertEqualityComparer(); + if (!keyComparer.Equals(expected.Value.Key, actual.Value.Key)) + return; + + var valueComparer = new AssertEqualityComparer(); + if (!valueComparer.Equals(expected.Value.Value, actual.Value.Value)) + return; + + throw NotEqualException.ForEqualValues(ArgumentFormatter.Format(expected.Value), ArgumentFormatter.Format(actual.Value)); + } + + /// + /// Verifies that two objects are strictly not equal, using . + /// + /// The expected object + /// The actual object + public static void NotStrictEqual( +#if XUNIT_NULLABLE + object? expected, + object? actual) +#else + object expected, + object actual) +#endif + { + if (!object.Equals(expected, actual)) + return; + + throw NotStrictEqualException.ForEqualValues( + ArgumentFormatter.Format(expected), + ArgumentFormatter.Format(actual) + ); + } + + /// + /// Verifies that two objects are strictly equal, using . + /// + /// The expected object + /// The actual object + public static void StrictEqual( +#if XUNIT_NULLABLE + object? expected, + object? actual) +#else + object expected, + object actual) +#endif + { + if (object.Equals(expected, actual)) + return; + + throw StrictEqualException.ForEqualValues( + ArgumentFormatter.Format(expected), + ArgumentFormatter.Format(actual) + ); + } + } +} + +#endif // XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts_reflection.cs b/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts_reflection.cs new file mode 100644 index 00000000000..3d069d7321c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/EqualityAsserts_reflection.cs @@ -0,0 +1,165 @@ +#if !XUNIT_AOT + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Xunit.Internal; +using Xunit.Sdk; + +#if XUNIT_OVERLOAD_RESOLUTION_PRIORITY +using System.Runtime.CompilerServices; +#endif + +namespace Xunit +{ + partial class Assert + { + static readonly Type typeofSet = typeof(ISet<>); + + /// + /// Verifies that two arrays of un-managed type T are equal, using Span<T>.SequenceEqual. + /// This can be significantly faster than generic enumerables, when the collections are actually + /// equal, because the system can optimize packed-memory comparisons for value type arrays. + /// + /// The type of items whose arrays are to be compared + /// The expected value + /// The value to be compared against + /// + /// If fails, a call + /// to is made, to provide a more meaningful error message. + /// + public static void Equal( +#if XUNIT_NULLABLE + T[]? expected, + T[]? actual) + where T : unmanaged, IEquatable +#else + T[] expected, + T[] actual) + where T : IEquatable +#endif + { + if (expected == null && actual == null) + return; + + if (expected == null || actual == null || !expected.AsSpan().SequenceEqual(actual)) + // Call into Equal (even though we'll re-enumerate) so we get proper formatting + // of the sequence, including the "first mismatch" pointer + Equal(expected, actual); + } + +#if XUNIT_NULLABLE + static string? GetCollectionDisplay( + Type? expectedType, + Type? expectedTypeDefinition, + Type? actualType, + Type? actualTypeDefinition) +#else + static string GetCollectionDisplay( + Type expectedType, + Type expectedTypeDefinition, + Type actualType, + Type actualTypeDefinition) +#endif + { + var expectedInterfaceTypeDefinitions = expectedType?.GetInterfaces().Where(i => i.IsGenericType).Select(i => i.GetGenericTypeDefinition()); + var actualInterfaceTypeDefinitions = actualType?.GetInterfaces().Where(i => i.IsGenericType).Select(i => i.GetGenericTypeDefinition()); + + if (expectedTypeDefinition == typeofDictionary && actualTypeDefinition == typeofDictionary) + return "Dictionaries"; + else if (expectedTypeDefinition == typeofHashSet && actualTypeDefinition == typeofHashSet) + return "HashSets"; +#pragma warning disable CA1508 + else if (expectedInterfaceTypeDefinitions != null && actualInterfaceTypeDefinitions != null && expectedInterfaceTypeDefinitions.Contains(typeofSet) && actualInterfaceTypeDefinitions.Contains(typeofSet)) +#pragma warning restore CA1508 + return "Sets"; + + return null; + } + + /// + /// Verifies that two arrays of un-managed type T are not equal, using Span<T>.SequenceEqual. + /// + /// The type of items whose arrays are to be compared + /// The expected value + /// The value to be compared against + public static void NotEqual( +#if XUNIT_NULLABLE + T[]? expected, + T[]? actual) + where T : unmanaged, IEquatable +#else + T[] expected, + T[] actual) + where T : IEquatable +#endif + { + // Call into NotEqual so we get proper formatting of the sequence + if (expected == null && actual == null) + NotEqual(expected, actual); + if (expected == null || actual == null) + return; + if (expected.AsSpan().SequenceEqual(actual)) + NotEqual(expected, actual); + } + + /// + /// Verifies that two objects are strictly not equal, using the type's default comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + public static void NotStrictEqual( +#if XUNIT_NULLABLE + T? expected, + T? actual) +#else + T expected, + T actual) +#endif + { + if (!EqualityComparer.Default.Equals(expected, actual)) + return; + + throw NotStrictEqualException.ForEqualValues( + ArgumentFormatter.Format(expected), + ArgumentFormatter.Format(actual) + ); + } + + /// + /// Verifies that two objects are strictly equal, using the type's default comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + public static void StrictEqual( +#if XUNIT_NULLABLE + T? expected, + T? actual) +#else + T expected, + T actual) +#endif + { + if (EqualityComparer.Default.Equals(expected, actual)) + return; + + throw StrictEqualException.ForEqualValues( + ArgumentFormatter.Format(expected), + ArgumentFormatter.Format(actual) + ); + } + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/EquivalenceAsserts_aot.cs b/src/Microsoft.DotNet.XUnitAssert/src/EquivalenceAsserts_aot.cs new file mode 100644 index 00000000000..093ff3c544c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/EquivalenceAsserts_aot.cs @@ -0,0 +1,105 @@ +#if XUNIT_AOT + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.ComponentModel; +using System.Linq.Expressions; + +namespace Xunit +{ + partial class Assert + { + /// + /// Assert.Equivalent is not supported in Native AOT due to reflection requirements. + /// + [Obsolete("Assert.Equivalent is not supported in Native AOT", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void Equivalent( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + bool strict = false) => + throw new NotSupportedException("Assert.Equivalent is not supported in Native AOT"); + + /// + /// Assert.EquivalentWithExclusions is not supported in Native AOT due to reflection requirements. + /// + [Obsolete("Assert.EquivalentWithExclusions is not supported in Native AOT", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void EquivalentWithExclusions( +#if XUNIT_NULLABLE + object? expected, +#else + object expected, +#endif + T actual, +#if XUNIT_NULLABLE + params Expression>[] exclusionExpressions) => +#else + params Expression>[] exclusionExpressions) => +#endif + throw new NotSupportedException("Assert.Equivalent is not supported in Native AOT"); + + /// + /// Assert.EquivalentWithExclusions is not supported in Native AOT due to reflection requirements. + /// + [Obsolete("Assert.EquivalentWithExclusions is not supported in Native AOT", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void EquivalentWithExclusions( +#if XUNIT_NULLABLE + object? expected, +#else + object expected, +#endif + T actual, + bool strict, +#if XUNIT_NULLABLE + params Expression>[] exclusionExpressions) => +#else + params Expression>[] exclusionExpressions) => +#endif + throw new NotSupportedException("Assert.Equivalent is not supported in Native AOT"); + + /// + /// Assert.EquivalentWithExclusions is not supported in Native AOT due to reflection requirements. + /// + [Obsolete("Assert.EquivalentWithExclusions is not supported in Native AOT", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void EquivalentWithExclusions( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + params string[] exclusionExpressions) => + throw new NotSupportedException("Assert.Equivalent is not supported in Native AOT"); + + /// + /// Assert.EquivalentWithExclusions is not supported in Native AOT due to reflection requirements. + /// + [Obsolete("Assert.EquivalentWithExclusions is not supported in Native AOT", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void EquivalentWithExclusions( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + bool strict, + params string[] exclusionExpressions) => + throw new NotSupportedException("Assert.Equivalent is not supported in Native AOT"); + } +} + +#endif // XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/EquivalenceAsserts_reflection.cs b/src/Microsoft.DotNet.XUnitAssert/src/EquivalenceAsserts_reflection.cs new file mode 100644 index 00000000000..17a3a35367b --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/EquivalenceAsserts_reflection.cs @@ -0,0 +1,179 @@ +#if !XUNIT_AOT + +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Linq.Expressions; +using Xunit.Internal; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that two objects are equivalent, using a default comparer. This comparison is done + /// without regard to type, and only inspects public property and field values for individual + /// equality. Deep equivalence tests (meaning, property or fields which are themselves complex + /// types) are supported. + /// + /// + /// With strict mode off, object comparison allows to have extra public + /// members that aren't part of , and collection comparison allows + /// to have more data in it than is present in ; + /// with strict mode on, those rules are tightened to require exact member list (for objects) or + /// data (for collections). + /// + /// The expected value + /// The actual value + /// A flag which enables strict comparison mode + public static void Equivalent( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + bool strict = false) + { + var ex = AssertHelper.VerifyEquivalence(expected, actual, strict); + if (ex != null) + throw ex; + } + + /// + /// Verifies that two objects are equivalent, using a default comparer. This comparison is done + /// without regard to type, and only inspects public property and field values for individual + /// equality. Deep equivalence tests (meaning, property or fields which are themselves complex + /// types) are supported. Members can be excluded from the comparison by passing them as + /// expressions via (using lambda expressions). + /// + /// The type of the actual value + /// The expected value + /// The actual value + /// The expressions for exclusions + public static void EquivalentWithExclusions( +#if XUNIT_NULLABLE + object? expected, +#else + object expected, +#endif + T actual, +#if XUNIT_NULLABLE + params Expression>[] exclusionExpressions) => +#else + params Expression>[] exclusionExpressions) => +#endif + EquivalentWithExclusions(expected, actual, strict: false, exclusionExpressions); + + /// + /// Verifies that two objects are equivalent, using a default comparer. This comparison is done + /// without regard to type, and only inspects public property and field values for individual + /// equality. Deep equivalence tests (meaning, property or fields which are themselves complex + /// types) are supported. Members can be excluded from the comparison by passing them as + /// expressions via (using lambda expressions). + /// + /// + /// With strict mode off, object comparison allows to have extra public + /// members that aren't part of , and collection comparison allows + /// to have more data in it than is present in ; + /// with strict mode on, those rules are tightened to require exact member list (for objects) or + /// data (for collections). + /// + /// The type of the actual value + /// The expected value + /// The actual value + /// A flag which enables strict comparison mode + /// The expressions for exclusions + public static void EquivalentWithExclusions( +#if XUNIT_NULLABLE + object? expected, +#else + object expected, +#endif + T actual, + bool strict, +#if XUNIT_NULLABLE + params Expression>[] exclusionExpressions) +#else + params Expression>[] exclusionExpressions) +#endif + { + var exclusions = AssertHelper.ParseExclusionExpressions(exclusionExpressions); + + var ex = AssertHelper.VerifyEquivalence(expected, actual, strict, exclusions); + if (ex != null) + throw ex; + } + + /// + /// Verifies that two objects are equivalent, using a default comparer. This comparison is done + /// without regard to type, and only inspects public property and field values for individual + /// equality. Deep equivalence tests (meaning, property or fields which are themselves complex + /// types) are supported. Members can be excluded from the comparison by passing them as + /// expressions via (using "Member.SubMember.SubSubMember" + /// form). + /// + /// The expected value + /// The actual value + /// The expressions for exclusions. This should be provided + /// in "Member.SubMember.SubSubMember" form for deep exclusions. + public static void EquivalentWithExclusions( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + params string[] exclusionExpressions) => + EquivalentWithExclusions(expected, actual, strict: false, exclusionExpressions); + + /// + /// Verifies that two objects are equivalent, using a default comparer. This comparison is done + /// without regard to type, and only inspects public property and field values for individual + /// equality. Deep equivalence tests (meaning, property or fields which are themselves complex + /// types) are supported. Members can be excluded from the comparison by passing them as + /// expressions via (using "Member.SubMember.SubSubMember" + /// form). + /// + /// + /// With strict mode off, object comparison allows to have extra public + /// members that aren't part of , and collection comparison allows + /// to have more data in it than is present in ; + /// with strict mode on, those rules are tightened to require exact member list (for objects) or + /// data (for collections). + /// + /// The expected value + /// The actual value + /// A flag which enables strict comparison mode + /// The expressions for exclusions. This should be provided + /// in "Member1.Member2.Member3" form for deep exclusions. + public static void EquivalentWithExclusions( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + bool strict, + params string[] exclusionExpressions) + { + var exclusions = AssertHelper.ParseExclusionExpressions(exclusionExpressions); + + var ex = AssertHelper.VerifyEquivalence(expected, actual, strict, exclusions); + if (ex != null) + throw ex; + } + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/EventAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/EventAsserts.cs new file mode 100644 index 00000000000..355bf6d492c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/EventAsserts.cs @@ -0,0 +1,643 @@ +#pragma warning disable CA1034 // Nested types should not be visible +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task +#pragma warning disable IDE0290 // Use primary constructor + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8600 +#pragma warning disable CS8603 +#pragma warning disable CS8622 +#pragma warning disable CS8625 +#endif + +using System; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that an event is raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// Thrown when the expected event was not raised. + public static void Raises( + Action attach, + Action detach, + Action testCode) + { + if (!RaisesInternal(attach, detach, testCode)) + throw RaisesException.ForNoEvent(); + } + + /// + /// Verifies that an event with the exact event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static RaisedEvent Raises( + Action> attach, + Action> detach, + Action testCode) + { + var raisedEvent = RaisesInternal(attach, detach, testCode) ?? throw RaisesException.ForNoEvent(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + /// + /// Verifies that an event with the exact event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static RaisedEvent Raises( + Action> attach, + Action> detach, + Action testCode) + { + var raisedEvent = RaisesInternal(attach, detach, testCode) ?? throw RaisesException.ForNoEvent(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + /// + /// Verifies that an event with the exact event args is raised. + /// + /// The type of the event arguments to expect + /// Code returning the raised event + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static RaisedEvent Raises( +#if XUNIT_NULLABLE + Func?> handler, +#else + Func> handler, +#endif + Action attach, + Action detach, + Action testCode) + { + var raisedEvent = RaisesInternal(handler, attach, detach, testCode) ?? throw RaisesException.ForNoEvent(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + /// + /// Verifies that an event is raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static RaisedEvent RaisesAny( + Action attach, + Action detach, + Action testCode) => + RaisesInternal(attach, detach, testCode) ?? throw RaisesAnyException.ForNoEvent(typeof(EventArgs)); + + /// + /// Verifies that an event with the exact or a derived event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static RaisedEvent RaisesAny( + Action> attach, + Action> detach, + Action testCode) => + RaisesInternal(attach, detach, testCode) ?? throw RaisesAnyException.ForNoEvent(typeof(T)); + + /// + /// Verifies that an event with the exact or a derived event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static RaisedEvent RaisesAny( + Action> attach, + Action> detach, + Action testCode) => + RaisesInternal(attach, detach, testCode) ?? throw RaisesAnyException.ForNoEvent(typeof(T)); + + /// + /// Verifies that an event is raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAnyAsync( + Action attach, + Action detach, + Func testCode) => + await RaisesAsyncInternal(attach, detach, testCode) ?? throw RaisesAnyException.ForNoEvent(typeof(EventArgs)); + + /// + /// Verifies that an event with the exact or a derived event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAnyAsync( + Action> attach, + Action> detach, + Func testCode) => + await RaisesAsyncInternal(attach, detach, testCode) ?? throw RaisesAnyException.ForNoEvent(typeof(T)); + + /// + /// Verifies that an event with the exact or a derived event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAnyAsync( + Action> attach, + Action> detach, + Func testCode) => + await RaisesAsyncInternal(attach, detach, testCode) ?? throw RaisesAnyException.ForNoEvent(typeof(T)); + + /// + /// Verifies that an event is raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task RaisesAsync( + Action attach, + Action detach, + Func testCode) + { + if (!await RaisesAsyncInternal(attach, detach, testCode)) + throw RaisesException.ForNoEvent(); + } + + /// + /// Verifies that an event with the exact event args (and not a derived type) is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAsync( + Action> attach, + Action> detach, + Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode) ?? throw RaisesException.ForNoEvent(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + /// + /// Verifies that an event with the exact event args (and not a derived type) is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAsync( + Action> attach, + Action> detach, + Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode) ?? throw RaisesException.ForNoEvent(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + // Helpers + + static bool RaisesInternal( + Action attach, + Action detach, + Action testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + var result = false; + void handler() => result = true; + + attach(handler); + testCode(); + detach(handler); + return result; + } + +#if XUNIT_NULLABLE + static RaisedEvent? RaisesInternal( +#else + static RaisedEvent RaisesInternal( +#endif + Action attach, + Action detach, + Action testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + var raisedEvent = default(RaisedEvent); +#if XUNIT_NULLABLE + void handler(object? s, EventArgs args) => +#else + void handler(object s, EventArgs args) => +#endif + raisedEvent = new RaisedEvent(s, args); + + attach(handler); + testCode(); + detach(handler); + return raisedEvent; + } + +#if XUNIT_NULLABLE + static RaisedEvent? RaisesInternal( +#else + static RaisedEvent RaisesInternal( +#endif + Action> attach, + Action> detach, + Action testCode) + { + var raisedEvent = default(RaisedEvent); + void handler(T args) => raisedEvent = new RaisedEvent(args); + + return RaisesInternal( + () => raisedEvent, + () => attach(handler), + () => detach(handler), + testCode); + } + +#if XUNIT_NULLABLE + static RaisedEvent? RaisesInternal( +#else + static RaisedEvent RaisesInternal( +#endif + Action> attach, + Action> detach, + Action testCode) + { + var raisedEvent = default(RaisedEvent); +#if XUNIT_NULLABLE + void handler(object? s, T args) => +#else + void handler(object s, T args) => +#endif + raisedEvent = new RaisedEvent(s, args); + + return RaisesInternal( + () => raisedEvent, + () => attach(handler), + () => detach(handler), + testCode); + } + +#if XUNIT_NULLABLE + static RaisedEvent? RaisesInternal( + Func?> handler, +#else + static RaisedEvent RaisesInternal( + Func> handler, +#endif + Action attach, + Action detach, + Action testCode) + { + GuardArgumentNotNull(nameof(handler), handler); + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + attach(); + testCode(); + detach(); + return handler(); + } + + static async Task RaisesAsyncInternal( + Action attach, + Action detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + var result = false; + void handler() => result = true; + + attach(handler); + await testCode(); + detach(handler); + return result; + } + +#if XUNIT_NULLABLE + static async Task?> RaisesAsyncInternal( +#else + static async Task> RaisesAsyncInternal( +#endif + Action attach, + Action detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + var raisedEvent = default(RaisedEvent); +#if XUNIT_NULLABLE + void handler(object? s, EventArgs args) => +#else + void handler(object s, EventArgs args) => +#endif + raisedEvent = new RaisedEvent(s, args); + + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + +#if XUNIT_NULLABLE + static async Task?> RaisesAsyncInternal( +#else + static async Task> RaisesAsyncInternal( +#endif + Action> attach, + Action> detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + var raisedEvent = default(RaisedEvent); + void handler(T args) => raisedEvent = new RaisedEvent(args); + + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + +#if XUNIT_NULLABLE + static async Task?> RaisesAsyncInternal( +#else + static async Task> RaisesAsyncInternal( +#endif + Action> attach, + Action> detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + var raisedEvent = default(RaisedEvent); +#if XUNIT_NULLABLE + void handler(object? s, T args) => +#else + void handler(object s, T args) => +#endif + raisedEvent = new RaisedEvent(s, args); + + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + + /// + /// Verifies that an event is not raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// Thrown when an unexpected event was raised. + public static void NotRaisedAny( + Action attach, + Action detach, + Action testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + if (RaisesInternal(attach, detach, testCode)) + throw NotRaisesException.ForUnexpectedEvent(); + } + + /// + /// Verifies that an event with the exact or a derived event args is not raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// Thrown when an unexpected event was raised. + public static void NotRaisedAny( + Action> attach, + Action> detach, + Action testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + if (RaisesInternal(attach, detach, testCode) != null) + throw NotRaisesException.ForUnexpectedEvent(typeof(T)); + } + + /// + /// Verifies that an event with the exact or a derived event args is not raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// Thrown when an unexpected event was raised. + public static void NotRaisedAny( + Action> attach, + Action> detach, + Action testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + if (RaisesInternal(attach, detach, testCode) != null) + throw NotRaisesException.ForUnexpectedEvent(typeof(T)); + } + + /// + /// Verifies that an event is not raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// Thrown when an unexpected event was raised. + public static async Task NotRaisedAnyAsync( + Action attach, + Action detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + if (await RaisesAsyncInternal(attach, detach, testCode)) + throw NotRaisesException.ForUnexpectedEvent(); + } + + /// + /// Verifies that an event with the exact or a derived event args is not raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// Thrown when an unexpected event was raised. + public static async Task NotRaisedAnyAsync( + Action> attach, + Action> detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + if (await RaisesAsyncInternal(attach, detach, testCode) != null) + throw NotRaisesException.ForUnexpectedEvent(typeof(T)); + } + + /// + /// Verifies that an event with the exact or a derived event args is not raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// Thrown when an unexpected event was raised. + public static async Task NotRaisedAnyAsync( + Action> attach, + Action> detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + + if (await RaisesAsyncInternal(attach, detach, testCode) != null) + throw NotRaisesException.ForUnexpectedEvent(typeof(T)); + } + + /// + /// Represents a raised event after the fact. + /// + /// The type of the event arguments. +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class RaisedEvent + { + /// + /// The sender of the event. When the event is recorded via rather + /// than , this value will always be , + /// since there is no sender value when using actions. + /// +#if XUNIT_NULLABLE + public object? Sender { get; } +#else + public object Sender { get; } +#endif + + /// + /// The event arguments. + /// + public T Arguments { get; } + + /// + /// Creates a new instance of the class. + /// + /// The event arguments + public RaisedEvent(T args) : + this(null, args) + { } + + /// + /// Creates a new instance of the class. + /// + /// The sender of the event. + /// The event arguments + public RaisedEvent( +#if XUNIT_NULLABLE + object? sender, +#else + object sender, +#endif + T args) + { + Sender = sender; + Arguments = args; + } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/ExceptionAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/ExceptionAsserts.cs new file mode 100644 index 00000000000..e2608f30804 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/ExceptionAsserts.cs @@ -0,0 +1,527 @@ +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#pragma warning disable CS8625 +#endif + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + public static Exception Throws( + Type exceptionType, + Action testCode) => + ThrowsImpl(exceptionType, RecordException(testCode)); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static Exception Throws( + Type exceptionType, + Action testCode, +#if XUNIT_NULLABLE + Func inspector) => +#else + Func inspector) => +#endif + ThrowsImpl(exceptionType, RecordException(testCode), inspector); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + public static Exception Throws( + Type exceptionType, +#if XUNIT_NULLABLE + Func testCode) => +#else + Func testCode) => +#endif + ThrowsImpl(exceptionType, RecordException(testCode, nameof(ThrowsAsync))); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static Exception Throws( + Type exceptionType, +#if XUNIT_NULLABLE + Func testCode, + Func inspector) => +#else + Func testCode, + Func inspector) => +#endif + ThrowsImpl(exceptionType, RecordException(testCode, nameof(ThrowsAsync)), inspector); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] + public static Exception Throws( + Type exceptionType, + Func testCode) + { + throw new NotSupportedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] + public static Exception Throws( + Type exceptionType, + Func testCode, +#if XUNIT_NULLABLE + Func inspector) +#else + Func inspector) +#endif + { + throw new NotSupportedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); + } + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + public static T Throws(Action testCode) + where T : Exception => + (T)ThrowsImpl(typeof(T), RecordException(testCode)); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static T Throws( + Action testCode, +#if XUNIT_NULLABLE + Func inspector) +#else + Func inspector) +#endif + where T : Exception => + (T)ThrowsImpl(typeof(T), RecordException(testCode), ex => inspector((T)ex)); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful +#if XUNIT_NULLABLE + public static T Throws(Func testCode) +#else + public static T Throws(Func testCode) +#endif + where T : Exception => + (T)ThrowsImpl(typeof(T), RecordException(testCode, nameof(ThrowsAsync))); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static T Throws( +#if XUNIT_NULLABLE + Func testCode, + Func inspector) +#else + Func testCode, + Func inspector) +#endif + where T : Exception => + (T)ThrowsImpl(typeof(T), RecordException(testCode, nameof(ThrowsAsync)), ex => inspector((T)ex)); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] + public static T Throws(Func testCode) + where T : Exception + { + throw new NotSupportedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] + public static T Throws( + Func testCode, +#if XUNIT_NULLABLE + Func inspector) +#else + Func inspector) +#endif + where T : Exception + { + throw new NotSupportedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); + } + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type), where the exception + /// derives from and has the given parameter name. + /// + /// The parameter name that is expected to be in the exception + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + public static T Throws( +#if XUNIT_NULLABLE + string? paramName, +#else + string paramName, +#endif + Action testCode) + where T : ArgumentException + { + var ex = Throws(testCode); + + if (paramName != ex.ParamName) + throw ThrowsException.ForIncorrectParameterName(typeof(T), paramName, ex.ParamName); + + return ex; + } + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type), where the exception + /// derives from and has the given parameter name. + /// + /// The parameter name that is expected to be in the exception + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + public static T Throws( +#if XUNIT_NULLABLE + string? paramName, + Func testCode) +#else + string paramName, + Func testCode) +#endif + where T : ArgumentException + { + var ex = Throws(testCode); + + if (paramName != ex.ParamName) + throw ThrowsException.ForIncorrectParameterName(typeof(T), paramName, ex.ParamName); + + return ex; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] + public static T Throws( +#if XUNIT_NULLABLE + string? paramName, +#else + string paramName, +#endif + Func testCode) + where T : ArgumentException + { + throw new NotSupportedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); + } + + /// + /// Verifies that the exact exception or a derived exception type is thrown. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + public static T ThrowsAny(Action testCode) + where T : Exception => + (T)ThrowsAnyImpl(typeof(T), RecordException(testCode)); + + /// + /// Verifies that the exact exception or a derived exception type is thrown. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static T ThrowsAny( + Action testCode, +#if XUNIT_NULLABLE + Func inspector) +#else + Func inspector) +#endif + where T : Exception => + (T)ThrowsAnyImpl(typeof(T), RecordException(testCode), ex => inspector((T)ex)); + + /// + /// Verifies that the exact exception or a derived exception type is thrown. + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful +#if XUNIT_NULLABLE + public static T ThrowsAny(Func testCode) +#else + public static T ThrowsAny(Func testCode) +#endif + where T : Exception => + (T)ThrowsAnyImpl(typeof(T), RecordException(testCode, nameof(ThrowsAnyAsync))); + + /// + /// Verifies that the exact exception or a derived exception type is thrown. + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static T ThrowsAny( +#if XUNIT_NULLABLE + Func testCode, + Func inspector) +#else + Func testCode, + Func inspector) +#endif + where T : Exception => + (T)ThrowsAnyImpl(typeof(T), RecordException(testCode, nameof(ThrowsAnyAsync)), ex => inspector((T)ex)); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAnyAsync (and await the result) when testing async code.", true)] + public static T ThrowsAny(Func testCode) + where T : Exception + { + throw new NotSupportedException("You must call Assert.ThrowsAnyAsync (and await the result) when testing async code."); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAnyAsync (and await the result) when testing async code.", true)] + public static T ThrowsAny( + Func testCode, +#if XUNIT_NULLABLE + Func inspector) +#else + Func inspector) +#endif + where T : Exception + { + throw new NotSupportedException("You must call Assert.ThrowsAnyAsync (and await the result) when testing async code."); + } + + /// + /// Verifies that the exact exception or a derived exception type is thrown. + /// + /// The type of the exception expected to be thrown + /// A delegate to the task to be tested + /// The exception that was thrown, when successful + public static async Task ThrowsAnyAsync(Func testCode) + where T : Exception => + (T)ThrowsAnyImpl(typeof(T), await RecordExceptionAsync(testCode)); + + /// + /// Verifies that the exact exception or a derived exception type is thrown. + /// + /// The type of the exception expected to be thrown + /// A delegate to the task to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static async Task ThrowsAnyAsync( + Func testCode, +#if XUNIT_NULLABLE + Func inspector) +#else + Func inspector) +#endif + where T : Exception => + (T)ThrowsAnyImpl(typeof(T), await RecordExceptionAsync(testCode), ex => inspector((T)ex)); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the task to be tested + /// The exception that was thrown, when successful + public static async Task ThrowsAsync( + Type exceptionType, + Func testCode) => + ThrowsImpl(exceptionType, await RecordExceptionAsync(testCode)); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the task to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static async Task ThrowsAsync( + Type exceptionType, + Func testCode, +#if XUNIT_NULLABLE + Func inspector) => +#else + Func inspector) => +#endif + ThrowsImpl(exceptionType, await RecordExceptionAsync(testCode), inspector); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the task to be tested + /// The exception that was thrown, when successful + public static async Task ThrowsAsync(Func testCode) + where T : Exception => + (T)ThrowsImpl(typeof(T), await RecordExceptionAsync(testCode)); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the task to be tested + /// A function which inspects the exception to determine if it's + /// valid or not. Returns if the exception is valid, or a message if it's not. + /// The exception that was thrown, when successful + public static async Task ThrowsAsync( + Func testCode, +#if XUNIT_NULLABLE + Func inspector) +#else + Func inspector) +#endif + where T : Exception => + (T)ThrowsImpl(typeof(T), await RecordExceptionAsync(testCode), ex => inspector((T)ex)); + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type), where the exception + /// derives from and has the given parameter name. + /// + /// The parameter name that is expected to be in the exception + /// A delegate to the task to be tested + /// The exception that was thrown, when successful + public static async Task ThrowsAsync( +#if XUNIT_NULLABLE + string? paramName, +#else + string paramName, +#endif + Func testCode) + where T : ArgumentException + { + var ex = await ThrowsAsync(testCode); + + if (paramName != ex.ParamName) + throw ThrowsException.ForIncorrectParameterName(typeof(T), paramName, ex.ParamName); + + return ex; + } + + static Exception ThrowsAnyImpl( + Type exceptionType, +#if XUNIT_NULLABLE + Exception? exception, + Func? inspector = null) +#else + Exception exception, + Func inspector = null) +#endif + { + GuardArgumentNotNull(nameof(exceptionType), exceptionType); + + if (exception == null) + throw ThrowsAnyException.ForNoException(exceptionType); + + if (!exceptionType.IsAssignableFrom(exception.GetType())) + throw ThrowsAnyException.ForIncorrectExceptionType(exceptionType, exception); + + var message = default(string); + try + { + message = inspector?.Invoke(exception); + } + catch (Exception ex) + { + throw ThrowsAnyException.ForInspectorFailure("Exception thrown by inspector", ex); + } + + if (message != null) + throw ThrowsAnyException.ForInspectorFailure(message); + + return exception; + } + + static Exception ThrowsImpl( + Type exceptionType, +#if XUNIT_NULLABLE + Exception? exception, + Func? inspector = null) +#else + Exception exception, + Func inspector = null) +#endif + { + GuardArgumentNotNull(nameof(exceptionType), exceptionType); + + if (exception == null) + throw ThrowsException.ForNoException(exceptionType); + + if (!exceptionType.Equals(exception.GetType())) + throw ThrowsException.ForIncorrectExceptionType(exceptionType, exception); + + var message = default(string); + try + { + message = inspector?.Invoke(exception); + } + catch (Exception ex) + { + throw ThrowsException.ForInspectorFailure("Exception thrown by inspector", ex); + } + + if (message != null) + throw ThrowsException.ForInspectorFailure(message); + + return exception; + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/FailAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/FailAsserts.cs new file mode 100644 index 00000000000..04bd4c6c3f1 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/FailAsserts.cs @@ -0,0 +1,32 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8625 +#endif + +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ + partial class Assert + { + /// + /// Indicates that the test should immediately fail. + /// + /// The optional failure message +#if XUNIT_NULLABLE + [DoesNotReturn] + public static void Fail(string? message = null) => +#else + public static void Fail(string message = null) => +#endif + throw FailException.ForFailure(message); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Guards.cs b/src/Microsoft.DotNet.XUnitAssert/src/Guards.cs new file mode 100644 index 00000000000..614cfcd9f56 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Guards.cs @@ -0,0 +1,35 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ + partial class Assert + { + /// +#if XUNIT_NULLABLE + [return: NotNull] +#endif + internal static T GuardArgumentNotNull( + string argName, +#if XUNIT_NULLABLE + [NotNull] T? argValue) +#else + T argValue) +#endif + { + if (argValue == null) + throw new ArgumentNullException(argName.TrimStart('@')); + + return argValue; + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/IdentityAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/IdentityAsserts.cs new file mode 100644 index 00000000000..c96e4891231 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/IdentityAsserts.cs @@ -0,0 +1,54 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that two objects are not the same instance. + /// + /// The expected object instance + /// The actual object instance + /// Thrown when the objects are the same instance + public static void NotSame( +#if XUNIT_NULLABLE + object? expected, + object? actual) +#else + object expected, + object actual) +#endif + { + if (object.ReferenceEquals(expected, actual)) + throw NotSameException.ForSameValues(); + } + + /// + /// Verifies that two objects are the same instance. + /// + /// The expected object instance + /// The actual object instance + /// Thrown when the objects are not the same instance + public static void Same( +#if XUNIT_NULLABLE + object? expected, + object? actual) +#else + object expected, + object actual) +#endif + { + if (!object.ReferenceEquals(expected, actual)) + throw SameException.ForFailure( + ArgumentFormatter.Format(expected), + ArgumentFormatter.Format(actual) + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/LICENSE.txt b/src/Microsoft.DotNet.XUnitAssert/src/LICENSE.txt new file mode 100644 index 00000000000..baa82080f13 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/LICENSE.txt @@ -0,0 +1,42 @@ +Unless otherwise specified in the file header, all code is licensed under Apache 2.0: + + Copyright (c) .NET Foundation and Contributors + All Rights Reserved + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Portions of this source code are licensed under the MIT license: + + The MIT License (MIT) + + Copyright (c) .NET Foundation and Contributors + + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/src/Microsoft.DotNet.XUnitAssert/src/MemoryAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/MemoryAsserts.cs new file mode 100644 index 00000000000..1eae48a4d1e --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/MemoryAsserts.cs @@ -0,0 +1,192 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + // While there is an implicit conversion operator from Memory to ReadOnlyMemory, the + // compiler still stumbles to do this automatically, which means we end up with lots of overloads + // with various arrangements of Memory and ReadOnlyMemory. + + // Also note that these classes will convert nulls into empty arrays automatically, since there + // is no way to represent a null readonly struct. + + /// + /// Verifies that a Memory contains a given sub-Memory + /// + /// The sub-Memory expected to be in the Memory + /// The Memory to be inspected + /// Thrown when the sub-Memory is not present inside the Memory + public static void Contains( + Memory expectedSubMemory, + Memory actualMemory) + where T : IEquatable => + Contains((ReadOnlyMemory)expectedSubMemory, (ReadOnlyMemory)actualMemory); + + /// + /// Verifies that a Memory contains a given sub-Memory + /// + /// The sub-Memory expected to be in the Memory + /// The Memory to be inspected + /// Thrown when the sub-Memory is not present inside the Memory + public static void Contains( + Memory expectedSubMemory, + ReadOnlyMemory actualMemory) + where T : IEquatable => + Contains((ReadOnlyMemory)expectedSubMemory, actualMemory); + + /// + /// Verifies that a Memory contains a given sub-Memory + /// + /// The sub-Memory expected to be in the Memory + /// The Memory to be inspected + /// Thrown when the sub-Memory is not present inside the Memory + public static void Contains( + ReadOnlyMemory expectedSubMemory, + Memory actualMemory) + where T : IEquatable => + Contains(expectedSubMemory, (ReadOnlyMemory)actualMemory); + + /// + /// Verifies that a Memory contains a given sub-Memory + /// + /// The sub-Memory expected to be in the Memory + /// The Memory to be inspected + /// Thrown when the sub-Memory is not present inside the Memory + public static void Contains( + ReadOnlyMemory expectedSubMemory, + ReadOnlyMemory actualMemory) + where T : IEquatable + { + GuardArgumentNotNull(nameof(expectedSubMemory), expectedSubMemory); + + if (actualMemory.Span.IndexOf(expectedSubMemory.Span) < 0) + throw ContainsException.ForSubMemoryNotFound( + CollectionTracker.FormatStart(expectedSubMemory.Span), + CollectionTracker.FormatStart(actualMemory.Span) + ); + } + + /// + /// Verifies that a Memory does not contain a given sub-Memory + /// + /// The sub-Memory expected not to be in the Memory + /// The Memory to be inspected + /// Thrown when the sub-Memory is present inside the Memory + public static void DoesNotContain( + Memory expectedSubMemory, + Memory actualMemory) + where T : IEquatable => + DoesNotContain((ReadOnlyMemory)expectedSubMemory, (ReadOnlyMemory)actualMemory); + + /// + /// Verifies that a Memory does not contain a given sub-Memory + /// + /// The sub-Memory expected not to be in the Memory + /// The Memory to be inspected + /// Thrown when the sub-Memory is present inside the Memory + public static void DoesNotContain( + Memory expectedSubMemory, + ReadOnlyMemory actualMemory) + where T : IEquatable => + DoesNotContain((ReadOnlyMemory)expectedSubMemory, actualMemory); + + /// + /// Verifies that a Memory does not contain a given sub-Memory + /// + /// The sub-Memory expected not to be in the Memory + /// The Memory to be inspected + /// Thrown when the sub-Memory is present inside the Memory + public static void DoesNotContain( + ReadOnlyMemory expectedSubMemory, + Memory actualMemory) + where T : IEquatable => + DoesNotContain(expectedSubMemory, (ReadOnlyMemory)actualMemory); + + /// + /// Verifies that a Memory does not contain a given sub-Memory + /// + /// The sub-Memory expected not to be in the Memory + /// The Memory to be inspected + /// Thrown when the sub-Memory is present inside the Memory + public static void DoesNotContain( + ReadOnlyMemory expectedSubMemory, + ReadOnlyMemory actualMemory) + where T : IEquatable + { + GuardArgumentNotNull(nameof(expectedSubMemory), expectedSubMemory); + + var expectedSpan = expectedSubMemory.Span; + var actualSpan = actualMemory.Span; + var idx = actualSpan.IndexOf(expectedSpan); + + if (idx > -1) + { + var formattedExpected = CollectionTracker.FormatStart(expectedSpan); + var formattedActual = CollectionTracker.FormatIndexedMismatch(actualSpan, idx, out var failurePointerIndent); + + throw DoesNotContainException.ForSubMemoryFound(formattedExpected, idx, failurePointerIndent, formattedActual); + } + } + + /// + /// Verifies that two Memory values are equivalent. + /// + /// The expected Memory value. + /// The actual Memory value. + /// Thrown when the Memory values are not equivalent. + public static void Equal( + Memory expectedMemory, + Memory actualMemory) + where T : IEquatable => + Equal((ReadOnlyMemory)expectedMemory, (ReadOnlyMemory)actualMemory); + + /// + /// Verifies that two Memory values are equivalent. + /// + /// The expected Memory value. + /// The actual Memory value. + /// Thrown when the Memory values are not equivalent. + public static void Equal( + Memory expectedMemory, + ReadOnlyMemory actualMemory) + where T : IEquatable => + Equal((ReadOnlyMemory)expectedMemory, actualMemory); + + /// + /// Verifies that two Memory values are equivalent. + /// + /// The expected Memory value. + /// The actual Memory value. + /// Thrown when the Memory values are not equivalent. + public static void Equal( + ReadOnlyMemory expectedMemory, + Memory actualMemory) + where T : IEquatable => + Equal(expectedMemory, (ReadOnlyMemory)actualMemory); + + /// + /// Verifies that two Memory values are equivalent. + /// + /// The expected Memory value. + /// The actual Memory value. + /// Thrown when the Memory values are not equivalent. + public static void Equal( + ReadOnlyMemory expectedMemory, + ReadOnlyMemory actualMemory) + where T : IEquatable + { + GuardArgumentNotNull(nameof(expectedMemory), expectedMemory); + + if (!expectedMemory.Span.SequenceEqual(actualMemory.Span)) + Equal(expectedMemory.Span.ToArray(), actualMemory.Span.ToArray(), new AssertEqualityComparer()); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Microsoft.DotNet.XUnitAssert.csproj b/src/Microsoft.DotNet.XUnitAssert/src/Microsoft.DotNet.XUnitAssert.csproj new file mode 100644 index 00000000000..123e73b8ada --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Microsoft.DotNet.XUnitAssert.csproj @@ -0,0 +1,29 @@ + + + + $(NetMinimum) + xunit.assert + $(MSBuildProjectName) + enable + enable + true + This package is a fork of xunit.assert that is AOT-compatible. + true + $(DefineConstants);XUNIT_NULLABLE;XUNIT_SPAN;XUNIT_IMMUTABLE_COLLECTIONS;XUNIT_AOT + true + true + true + + $(MSBuildThisFileDirectory)xunit.snk + 0024000004800000940000000602000000240000525341310004000001000100252e049addea87f30f99d6ed8ebc189bc05b8c9168765df08f86e0214471dc89844f1f4b9c4a26894d029465848771bc758fed20371280eda223a9f64ae05f48b320e4f0e20c4282dd701e985711bc33b5b9e6ab3fafab6cb78e220ee2b8e1550573e03f8ad665c051c63fbc5359d495d4b1c61024ef76ed9c1ebb471fed59c9 + 8d05b1bb7a6fdb6c + + $(XUnitV3Version.Split('-')[0]) + + + + + + + + diff --git a/src/Microsoft.DotNet.XUnitAssert/src/MultipleAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/MultipleAsserts.cs new file mode 100644 index 00000000000..82dd4960f35 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/MultipleAsserts.cs @@ -0,0 +1,79 @@ +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Runs multiple checks, collecting the exceptions from each one, and then bundles all failures + /// up into a single assertion failure. + /// + /// The individual assertions to run, as actions. + public static void Multiple(params Action[] checks) + { + if (checks == null || checks.Length == 0) + return; + + var exceptions = new List(); + + foreach (var check in checks) + try + { + check(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + + if (exceptions.Count == 0) + return; + if (exceptions.Count == 1) + ExceptionDispatchInfo.Capture(exceptions[0]).Throw(); + + throw MultipleException.ForFailures(exceptions); + } + + /// + /// Asynchronously runs multiple checks, collecting the exceptions from each one, and then bundles all failures + /// up into a single assertion failure. + /// + /// The individual assertions to run, as async actions. + public static async Task MultipleAsync(params Func[] checks) + { + if (checks == null || checks.Length == 0) + return; + + var exceptions = new List(); + + foreach (var check in checks) + try + { + await check(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + + if (exceptions.Count == 0) + return; + if (exceptions.Count == 1) + ExceptionDispatchInfo.Capture(exceptions[0]).Throw(); + + throw MultipleException.ForFailuresAsync(exceptions); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/NullAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/NullAsserts.cs new file mode 100644 index 00000000000..28d7387beb2 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/NullAsserts.cs @@ -0,0 +1,122 @@ +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA1720 // Identifier contains type name + +#if XUNIT_NULLABLE +#nullable enable +#endif + +#if XUNIT_POINTERS +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type +#endif + +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that an object reference is not null. + /// + /// The object to be validated + /// Thrown when the object reference is null +#if XUNIT_NULLABLE + public static void NotNull([NotNull] object? @object) +#else + public static void NotNull(object @object) +#endif + { + if (@object == null) + throw NotNullException.ForNullValue(); + } + +#if XUNIT_POINTERS + + /// + /// Verifies that an unmanaged pointer is not null. + /// + /// The type of the pointer + /// The pointer value +#if XUNIT_NULLABLE + public static unsafe void NotNull([NotNull] T* value) +#else + public static unsafe void NotNull(T* value) +#endif + { + if (value == null) + throw NotNullException.ForNullPointer(typeof(T)); + } + +#endif // XUNIT_POINTERS + + /// + /// Verifies that a nullable struct value is not null. + /// + /// The type of the struct + /// The value to e validated + /// The non- value + /// Thrown when the value is null +#if XUNIT_NULLABLE + public static T NotNull([NotNull] T? value) +#else + public static T NotNull(T? value) +#endif + where T : struct + { + if (!value.HasValue) + throw NotNullException.ForNullStruct(typeof(T)); + + return value.Value; + } + + /// + /// Verifies that an object reference is null. + /// + /// The object to be inspected + /// Thrown when the object reference is not null +#if XUNIT_NULLABLE + public static void Null([MaybeNull] object? @object) +#else + public static void Null(object @object) +#endif + { + if (@object != null) + throw NullException.ForNonNullValue(@object); + } + + /// + /// Verifies that a nullable struct value is null. + /// + /// The value to be inspected + /// Thrown when the value is not null + public static void Null(T? value) + where T : struct + { + if (value.HasValue) + throw NullException.ForNonNullStruct(typeof(T), value); + } + +#if XUNIT_POINTERS + + /// + /// Verifies that an unmanaged pointer is null. + /// + /// The type of the pointer + /// The pointer value +#if XUNIT_NULLABLE + public static unsafe void Null([NotNull] T* value) +#else + public static unsafe void Null(T* value) +#endif + { + if (value != null) + throw NullException.ForNonNullPointer(typeof(T)); + } + +#endif // XUNIT_POINTERS + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/PropertyAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/PropertyAsserts.cs new file mode 100644 index 00000000000..1c9c3b12a52 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/PropertyAsserts.cs @@ -0,0 +1,112 @@ +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA1720 // Identifier contains type name +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8622 +#endif + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that the provided object raised + /// as a result of executing the given test code. + /// + /// The object which should raise the notification + /// The property name for which the notification should be raised + /// The test code which should cause the notification to be raised + /// Thrown when the notification is not raised + public static void PropertyChanged( + INotifyPropertyChanged @object, + string propertyName, + Action testCode) + { + GuardArgumentNotNull(nameof(@object), @object); + GuardArgumentNotNull(nameof(propertyName), propertyName); + GuardArgumentNotNull(nameof(testCode), testCode); + + var propertyChangeHappened = false; + +#if XUNIT_NULLABLE + void handler(object? sender, PropertyChangedEventArgs args) => +#else + void handler(object sender, PropertyChangedEventArgs args) => +#endif + propertyChangeHappened = propertyChangeHappened || string.IsNullOrEmpty(args.PropertyName) || propertyName.Equals(args.PropertyName, StringComparison.OrdinalIgnoreCase); + + @object.PropertyChanged += handler; + + try + { + testCode(); + if (!propertyChangeHappened) + throw PropertyChangedException.ForUnsetProperty(propertyName); + } + finally + { + @object.PropertyChanged -= handler; + } + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.PropertyChangedAsync (and await the result) when testing async code.", true)] + public static void PropertyChanged( + INotifyPropertyChanged @object, + string propertyName, + Func testCode) + { + throw new NotSupportedException("You must call Assert.PropertyChangedAsync (and await the result) when testing async code."); + } + + /// + /// Verifies that the provided object raised + /// as a result of executing the given test code. + /// + /// The object which should raise the notification + /// The property name for which the notification should be raised + /// The test code which should cause the notification to be raised + /// Thrown when the notification is not raised + public static async Task PropertyChangedAsync( + INotifyPropertyChanged @object, + string propertyName, + Func testCode) + { + GuardArgumentNotNull(nameof(@object), @object); + GuardArgumentNotNull(nameof(propertyName), propertyName); + GuardArgumentNotNull(nameof(testCode), testCode); + + var propertyChangeHappened = false; + +#if XUNIT_NULLABLE + void handler(object? sender, PropertyChangedEventArgs args) => +#else + void handler(object sender, PropertyChangedEventArgs args) => +#endif + propertyChangeHappened = propertyChangeHappened || string.IsNullOrEmpty(args.PropertyName) || propertyName.Equals(args.PropertyName, StringComparison.OrdinalIgnoreCase); + + @object.PropertyChanged += handler; + + try + { + await testCode(); + if (!propertyChangeHappened) + throw PropertyChangedException.ForUnsetProperty(propertyName); + } + finally + { + @object.PropertyChanged -= handler; + } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/README.md b/src/Microsoft.DotNet.XUnitAssert/src/README.md new file mode 100644 index 00000000000..f2fe5c04532 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/README.md @@ -0,0 +1,139 @@ +# About This Project + +This project contains the xUnit.net assertion library source code, intended to be used as a Git submodule (or via the `xunit.v3.assert.source` NuGet package). + +Code here is built with `netstandard2.0` and `net8.0` within xUnit.net v3. At a minimum the code needs to be able to support `net472` and later for .NET Framework, and `net8.0` and later for .NET. The minimum (and default) C# version is 7.3, unless specific features require targeting later compilers. Additionally, we compile with the full Roslyn analyzer set enabled when building for v3, so you will frequently see conditional code and/or rules being disabled as appropriate. These constraints are supported by the [suggested contribution workflow](#suggested-contribution-workflow), which aims to make it easy to know when you've used unavailable features. + +This code includes assertions for immutable collections as well as the `Span` and `Memory` family of types. If you experience compiler errors related to these types, you may need to add references to the following NuGet packages: + +```xml + + + + +``` + +> _**Note:** If your PR requires a newer target framework or a newer C# language to build, please start a discussion in the related issue(s) before starting any work. PRs that arbitrarily use newer target frameworks and/or newer C# language features will need to be fixed; you may be asked to fix them, or we may fix them for you, or we may decline the PR (at our discretion)._ + +To open an issue for this project, please visit the [core xUnit.net project issue tracker](https://github.com/xunit/xunit/issues). + +## Annotations + +Whether you are using this repository via Git submodule or via the [source-based NuGet package](https://www.nuget.org/packages/xunit.assert.source), the following pre-processor directives can be used to influence the code contained in this repository: + +### `XUNIT_AOT` (min: C# 13, .NET 9) + +Define this compilation symbol to use assertions that are compatible with Native AOT. + +_Note: you must add_ `true` _to the property group of your project file._ + +### `XUNIT_NULLABLE` (min: C# 9.0) + +Define this compilation symbol to opt-in to support for nullable reference types and to enable the relevant nullability analysis annotations on method signatures. + +_Note: you must add_ `enable` _to the property group of your project file._ + +### `XUNIT_OVERLOAD_RESOLUTION_PRIORITY` (min: C# 13.0) + +Define this compilation symbol to opt-in to decorating assertion functions with [`[OverloadResolutionPriority]`](https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.overloadresolutionpriorityattribute) to help the compiler resolve competing ambiguous overloads. + +### `XUNIT_POINTERS` + +Define this compilation symbol to enable support for assertions related to unsafe pointers. + +_Note: you must add_ `true` _to the property group of your project file._ + +### `XUNIT_VISIBILITY_INTERNAL` + +By default, the `Assert` class has `public` visibility. This is appropriate for the default usage (as a shipped library). If your consumption of `Assert` via source is intended to be local to a single library, you should define `XUNIT_VISIBILITY_INTERNAL` to move the visibility of the `Assert` class to `internal`. + +## Suggested Contribution Workflow + +The pull request workflow for the assertion library is more complex than a typical single-repository project. The source code for the assertions live in this repository, and the source code for the unit tests live in the main repository: [`xunit/xunit`](https://github.com/xunit/xunit). + +This workflow makes it easier to work in your branches as well as ensuring that your PR build has a higher chance of succeeding. + +You will need a fork of both `xunit/assert.xunit` (this repository) and `xunit/xunit` (the main repository for xUnit.net). You will also need a local clone of `xunit/xunit`, which is where you will be doing all your work. _You do not need a clone of your `xunit/assert.xunit` fork, because we use Git submodules to bring both repositories together into a single folder._ + +### Before you start working + +1. In a command prompt, from the root of the repository, run: + + * `git submodule update --init` to ensure the Git submodule in `/src/xunit.v3.assert/Asserts` is initialized. + * `git switch main` + * `git pull origin --ff-only` to ensure that `main` is up to date. + * `git remote add fork https://github.com/yourusername/xunit` to point to your fork (update the URL as appropriate). + * `git fetch fork` to ensure that your `fork` remote is working. + * `git switch -c my-branch-name` to create a new branch for `xunit/xunit`. + + _Replace `my-branch-name` with whatever branch name you want. We suggest you put the general feature and the `xunit/xunit` issue number into the name, to help you track the work if you're planning to help with multiple issues. An example branch name might be something like `add-support-for-IAsyncEnumerable-2367`._ + +1. In a command prompt, from `/src/xunit.v3.assert/Asserts`, run: + + * `git switch main` + * `git pull origin --ff-only` to ensure that `main` is up to date. + * `git remote add fork https://github.com/yourusername/assert.xunit` to point to your fork (update the URL as appropriate). + * `git fetch fork` to ensure that your `fork` remote is working. + * `git switch -c my-branch-name` to create a new branch for `xunit/assert.xunit`. + + _You may use the same branch name that you used above, as these branches are in two different repositories; identical names won't conflict, and may help you keep your work straight if you are working on multiple issues._ + +### Create the code and test + +Open the solution in Visual Studio (or your preferred editor/IDE), and create your changes. The assertion changes will live in `/src/xunit.v3.assert/Asserts` and the tests will live in `/src/xunit.v3.assert.tests/Asserts`. In Visual Studio, the two projects you'll be working in are named `xunit.v3.assert` and `xunit.v3.assert.tests`. (You will see several `xunit.v3.assert.*` projects which ensure that the code you're writing correctly compiles in all the supported scenarios.) + +When the changes are complete, you can run `./build` from the root of the repository to run the full test suite that would normally be run by a PR. + +### When you're ready to submit the pull requests + +1. In a command prompt, from `/src/xunit.v3.assert/Asserts`, run: + + * `git add -A` + * `git commit` + * `git push fork my-branch-name` + + _This pushes the branch up to your fork for you to create the PR for `xunit/assert.xunit`. The push message will give you a link (something like `https://github.com/yourusername/assert.xunit/pull/new/my-new-branch`) to start the PR process. You may do that now. We do this folder first, because we need for the source to be pushed to get a commit reference for the next step._ + +1. In a command prompt, from the root of the repository, run the same three commands: + + * `git add -A` + * `git commit` + * `git push fork my-branch-name` + + _Just like the previous steps did, this pushes up your branch for the PR for `xunit/xunit`. Only do this after you have pushed your PR-ready changes for `xunit/assert.xunit`. You may now start the PR process for `xunit/xunit` as well, and it will include the reference to the new assertion code that you've already pushed._ + +A maintainer will review and merge your PRs, and automatically create equivalent updates to the `v2` branch so that your assertion changes will be made available for any potential future xUnit.net v2.x releases. + +_Please remember that all PRs require associated unit tests. You may be asked to write the tests if you create a PR without them. If you're not sure how to test the code in question, please feel free to open the PR and then mention that in the PR description, and someone will help you with this._ + +# About xUnit.net + +xUnit.net is a free, open source, community-focused unit testing tool for C#, F#, and Visual Basic. + +xUnit.net works with the [.NET SDK](https://dotnet.microsoft.com/download) command line tools, [Visual Studio](https://visualstudio.microsoft.com/), [Visual Studio Code](https://code.visualstudio.com/), [JetBrains Rider](https://www.jetbrains.com/rider/), [NCrunch](https://www.ncrunch.net/), and any development environment compatible with [Microsoft Testing Platform](https://learn.microsoft.com/dotnet/core/testing/microsoft-testing-platform-intro) (xUnit.net v3) or [VSTest](https://github.com/microsoft/vstest) (all versions of xUnit.net). + +xUnit.net is part of the [.NET Foundation](https://www.dotnetfoundation.org/) and operates under their [code of conduct](https://www.dotnetfoundation.org/code-of-conduct). It is licensed under [Apache 2](https://opensource.org/licenses/Apache-2.0) (an OSI approved license). The project is [governed](https://xunit.net/governance) by a Project Lead. + +For project documentation, please visit the [xUnit.net project home](https://xunit.net/). + +* _New to xUnit.net? Get started with the [.NET SDK](https://xunit.net/docs/getting-started/v3/getting-started)._ +* _Need some help building the source? See [BUILDING.md](https://github.com/xunit/xunit/tree/main/BUILDING.md)._ +* _Want to contribute to the project? See [CONTRIBUTING.md](https://github.com/xunit/.github/tree/main/CONTRIBUTING.md)._ +* _Want to contribute to the assertion library? See the [suggested contribution workflow](https://github.com/xunit/assert.xunit/tree/main/README.md#suggested-contribution-workflow) in the assertion library project, as it is slightly more complex due to code being spread across two GitHub repositories._ + +[![Powered by NDepend](https://raw.github.com/xunit/media/main/powered-by-ndepend-transparent.png)](http://www.ndepend.com/) + +## Latest Builds + +| | Latest stable | Latest CI ([how to use](https://xunit.net/docs/using-ci-builds)) | Build status +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ +| `xunit.v3` | [![](https://img.shields.io/nuget/v/xunit.v3.svg?logo=nuget)](https://www.nuget.org/packages/xunit.v3) | [![](https://img.shields.io/endpoint.svg?url=https://f.feedz.io/xunit/xunit/shield/xunit.v3/latest&logo=nuget&color=f58142)](https://feedz.io/org/xunit/repository/xunit/packages/xunit.v3) | [![](https://img.shields.io/endpoint.svg?url=https://actions-badge.atrox.dev/xunit/xunit/badge%3Fref%3Dmain&label=build)](https://actions-badge.atrox.dev/xunit/xunit/goto?ref=main) +| `xunit` | [![](https://img.shields.io/nuget/v/xunit.svg?logo=nuget)](https://www.nuget.org/packages/xunit) | [![](https://img.shields.io/endpoint.svg?url=https://f.feedz.io/xunit/xunit/shield/xunit/latest&logo=nuget&color=f58142)](https://feedz.io/org/xunit/repository/xunit/packages/xunit) | [![](https://img.shields.io/endpoint.svg?url=https://actions-badge.atrox.dev/xunit/xunit/badge%3Fref%3Dv2&label=build)](https://actions-badge.atrox.dev/xunit/xunit/goto?ref=v2) +| `xunit.analyzers` | [![](https://img.shields.io/nuget/v/xunit.analyzers.svg?logo=nuget)](https://www.nuget.org/packages/xunit.analyzers) | [![](https://img.shields.io/endpoint.svg?url=https://f.feedz.io/xunit/xunit/shield/xunit.analyzers/latest&logo=nuget&color=f58142)](https://feedz.io/org/xunit/repository/xunit/packages/xunit.analyzers) | [![](https://img.shields.io/endpoint.svg?url=https://actions-badge.atrox.dev/xunit/xunit.analyzers/badge%3Fref%3Dmain&label=build)](https://actions-badge.atrox.dev/xunit/xunit.analyzers/goto?ref=main) +| `xunit.runner.visualstudio` | [![](https://img.shields.io/nuget/v/xunit.runner.visualstudio.svg?logo=nuget)](https://www.nuget.org/packages/xunit.runner.visualstudio) | [![](https://img.shields.io/endpoint.svg?url=https://f.feedz.io/xunit/xunit/shield/xunit.runner.visualstudio/latest&logo=nuget&color=f58142)](https://feedz.io/org/xunit/repository/xunit/packages/xunit.runner.visualstudio) | [![](https://img.shields.io/endpoint.svg?url=https://actions-badge.atrox.dev/xunit/visualstudio.xunit/badge%3Fref%3Dmain&label=build)](https://actions-badge.atrox.dev/xunit/visualstudio.xunit/goto?ref=main) + +*For complete CI package lists, please visit the [feedz.io package search](https://feedz.io/org/xunit/repository/xunit/search). A free login is required.* + +## Sponsors + +Help support this project by becoming a sponsor through [GitHub Sponsors](https://github.com/sponsors/xunit). diff --git a/src/Microsoft.DotNet.XUnitAssert/src/RangeAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/RangeAsserts.cs new file mode 100644 index 00000000000..d3928345044 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/RangeAsserts.cs @@ -0,0 +1,96 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Collections.Generic; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that a value is within a given range. + /// + /// The type of the value to be compared + /// The actual value to be evaluated + /// The (inclusive) low value of the range + /// The (inclusive) high value of the range + /// Thrown when the value is not in the given range + public static void InRange( + T actual, + T low, + T high) + where T : IComparable => + InRange(actual, low, high, GetRangeComparer()); + + /// + /// Verifies that a value is within a given range, using a comparer. + /// + /// The type of the value to be compared + /// The actual value to be evaluated + /// The (inclusive) low value of the range + /// The (inclusive) high value of the range + /// The comparer used to evaluate the value's range + /// Thrown when the value is not in the given range + public static void InRange( + T actual, + T low, + T high, + IComparer comparer) + { + GuardArgumentNotNull(nameof(actual), actual); + GuardArgumentNotNull(nameof(low), low); + GuardArgumentNotNull(nameof(high), high); + GuardArgumentNotNull(nameof(comparer), comparer); + + if (comparer.Compare(low, actual) > 0 || comparer.Compare(actual, high) > 0) + throw InRangeException.ForValueNotInRange(actual, low, high); + } + + /// + /// Verifies that a value is not within a given range, using the default comparer. + /// + /// The type of the value to be compared + /// The actual value to be evaluated + /// The (inclusive) low value of the range + /// The (inclusive) high value of the range + /// Thrown when the value is in the given range + public static void NotInRange( + T actual, + T low, + T high) + where T : IComparable => + NotInRange(actual, low, high, GetRangeComparer()); + + /// + /// Verifies that a value is not within a given range, using a comparer. + /// + /// The type of the value to be compared + /// The actual value to be evaluated + /// The (inclusive) low value of the range + /// The (inclusive) high value of the range + /// The comparer used to evaluate the value's range + /// Thrown when the value is in the given range + public static void NotInRange( + T actual, + T low, + T high, + IComparer comparer) + { + GuardArgumentNotNull(nameof(actual), actual); + GuardArgumentNotNull(nameof(low), low); + GuardArgumentNotNull(nameof(high), high); + GuardArgumentNotNull(nameof(comparer), comparer); + + if (comparer.Compare(low, actual) <= 0 && comparer.Compare(actual, high) <= 0) + throw NotInRangeException.ForValueInRange(actual, low, high); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Record.cs b/src/Microsoft.DotNet.XUnitAssert/src/Record.cs new file mode 100644 index 00000000000..5ca5411d0ab --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Record.cs @@ -0,0 +1,150 @@ +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task +#pragma warning disable IDE0059 // Unnecessary assignment of a value + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#endif + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Threading.Tasks; + +namespace Xunit +{ + partial class Assert + { + /// + /// The contract for exceptions which indicate that something should be skipped rather than + /// failed is that exception message should start with this, and that any text following this + /// will be treated as the skip reason (for example, "$XunitDynamicSkip$This code can only run + /// on Linux") will result in a skipped test with the reason of "This code can only run + /// on Linux". + /// + const string DynamicSkipToken = "$XunitDynamicSkip$"; + + /// + /// Records any exception which is thrown by the given code. + /// + /// The code which may thrown an exception. + /// Returns the exception that was thrown by the code; null, otherwise. + /// + /// If the thrown exception is determined to be a "skip exception", it's not recorded, but + /// instead is allowed to escape this function uncaught. + /// +#if XUNIT_NULLABLE + protected static Exception? RecordException(Action testCode) +#else + protected static Exception RecordException(Action testCode) +#endif + { + GuardArgumentNotNull(nameof(testCode), testCode); + + try + { + testCode(); + return null; + } + catch (Exception ex) + { + if (ex.Message?.StartsWith(DynamicSkipToken, StringComparison.Ordinal) == true) + throw; + + return ex; + } + } + + /// + /// Records any exception which is thrown by the given code that has + /// a return value. Generally used for testing property accessors. + /// + /// The code which may thrown an exception. + /// The name of the async method the user should've called if they accidentally + /// passed in an async function + /// Returns the exception that was thrown by the code; null, otherwise. + /// + /// If the thrown exception is determined to be a "skip exception", it's not recorded, but + /// instead is allowed to escape this function uncaught. + /// +#if XUNIT_NULLABLE + protected static Exception? RecordException( + Func testCode, +#else + protected static Exception RecordException( + Func testCode, +#endif + string asyncMethodName) + { + GuardArgumentNotNull(nameof(testCode), testCode); + + var result = default(object); + + try + { + result = testCode(); + } + catch (Exception ex) + { + if (ex.Message?.StartsWith(DynamicSkipToken, StringComparison.Ordinal) == true) + throw; + + return ex; + } + + if (result is Task) + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "You must call Assert.{0} when testing async code", + asyncMethodName + ) + ); + + return null; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.RecordExceptionAsync (and await the result) when testing async code.", true)] + protected static Exception RecordException(Func testCode) + { + throw new NotSupportedException("You must call Assert.RecordExceptionAsync (and await the result) when testing async code."); + } + + /// + /// Records any exception which is thrown by the given task. + /// + /// The task which may thrown an exception. + /// Returns the exception that was thrown by the code; null, otherwise. + /// + /// If the thrown exception is determined to be a "skip exception", it's not recorded, but + /// instead is allowed to escape this function uncaught. + /// +#if XUNIT_NULLABLE + protected static async Task RecordExceptionAsync(Func testCode) +#else + protected static async Task RecordExceptionAsync(Func testCode) +#endif + { + GuardArgumentNotNull(nameof(testCode), testCode); + + try + { + await testCode(); + return null; + } + catch (Exception ex) + { + if (ex.Message?.StartsWith(DynamicSkipToken, StringComparison.Ordinal) == true) + throw; + + return ex; + } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter.cs new file mode 100644 index 00000000000..13637138815 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter.cs @@ -0,0 +1,542 @@ +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable IDE0019 // Use pattern matching +#pragma warning disable IDE0057 // Use range operator +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0300 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8600 +#pragma warning disable CS8602 +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#pragma warning disable CS8605 +#pragma warning disable CS8625 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Xunit.Internal; + +#if XUNIT_ARGUMENTFORMATTER_PRIVATE +namespace Xunit.Internal +#else +namespace Xunit.Sdk +#endif +{ + /// + /// Formats value for display in assertion messages and data-driven test display names. + /// +#if XUNIT_VISIBILITY_INTERNAL || XUNIT_ARGUMENTFORMATTER_PRIVATE + internal +#else + public +#endif + static partial class ArgumentFormatter + { + static readonly Lazy maxEnumerableLength = new Lazy( + () => GetEnvironmentValue(EnvironmentVariables.PrintMaxEnumerableLength, EnvironmentVariables.Defaults.PrintMaxEnumerableLength)); + static readonly Lazy maxObjectDepth = new Lazy( + () => GetEnvironmentValue(EnvironmentVariables.PrintMaxObjectDepth, EnvironmentVariables.Defaults.PrintMaxObjectDepth)); + static readonly Lazy maxObjectMemberCount = new Lazy( + () => GetEnvironmentValue(EnvironmentVariables.PrintMaxObjectMemberCount, EnvironmentVariables.Defaults.PrintMaxObjectMemberCount)); + static readonly Lazy maxStringLength = new Lazy( + () => GetEnvironmentValue(EnvironmentVariables.PrintMaxStringLength, EnvironmentVariables.Defaults.PrintMaxStringLength)); + + internal static readonly string EllipsisInBrackets = "[" + new string((char)0x00B7, 3) + "]"; + + // List of intrinsic types => C# type names + static readonly Dictionary TypeMappings = new Dictionary + { + { typeof(bool), "bool" }, + { typeof(byte), "byte" }, + { typeof(sbyte), "sbyte" }, + { typeof(char), "char" }, + { typeof(decimal), "decimal" }, + { typeof(double), "double" }, + { typeof(float), "float" }, + { typeof(int), "int" }, + { typeof(uint), "uint" }, + { typeof(long), "long" }, + { typeof(ulong), "ulong" }, + { typeof(object), "object" }, + { typeof(short), "short" }, + { typeof(ushort), "ushort" }, + { typeof(string), "string" }, + { typeof(IntPtr), "nint" }, + { typeof(UIntPtr), "nuint" }, + }; + + /// + /// Gets the ellipsis value (three middle dots, aka U+00B7). + /// + public static string Ellipsis { get; } = new string((char)0x00B7, 3); + + /// + /// Gets the maximum number of values printed for collections before truncation. + /// + public static int MaxEnumerableLength => maxEnumerableLength.Value; + + /// + /// Gets the maximum printing depth, in terms of objects before truncation. + /// + public static int MaxObjectDepth => maxObjectDepth.Value; + + /// + /// Gets the maximum number of items (properties or fields) printed in an object before truncation. + /// + public static int MaxObjectMemberCount => maxObjectMemberCount.Value; + + /// + /// Gets the maximum strength length before truncation. + /// + public static int MaxStringLength => maxStringLength.Value; + + /// + /// Escapes a string for printing, attempting to most closely model the value on how you would + /// enter the value in a C# string literal. That means control codes that are normally backslash + /// escaped (like "\n" for newline) are represented like that; all other control codes for ASCII + /// values under 32 are printed as "\xnn". + /// + /// The string value to be escaped + public static string EscapeString(string s) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(s); +#else + if (s == null) + throw new ArgumentNullException(nameof(s)); +#endif + + var builder = new StringBuilder(s.Length); + for (var i = 0; i < s.Length; i++) + { + var ch = s[i]; + + if (TryGetEscapeSequence(ch, out var escapeSequence)) + builder.Append(escapeSequence); + else if (ch < 32) // C0 control char + builder.AppendFormat(CultureInfo.CurrentCulture, @"\x{0}", (+ch).ToString("x2", CultureInfo.CurrentCulture)); + else if (char.IsSurrogatePair(s, i)) // should handle the case of ch being the last one + { + // For valid surrogates, append like normal + builder.Append(ch); + builder.Append(s[++i]); + } + // Check for stray surrogates/other invalid chars + else if (char.IsSurrogate(ch) || ch == '\uFFFE' || ch == '\uFFFF') + { + builder.AppendFormat(CultureInfo.CurrentCulture, @"\x{0}", (+ch).ToString("x4", CultureInfo.CurrentCulture)); + } + else + builder.Append(ch); // Append the char like normal + } + return builder.ToString(); + } + + /// + /// Formats a value for display. + /// + /// The value to be formatted + /// The optional printing depth (1 indicates a top-level value) + public static string Format( +#if XUNIT_NULLABLE + object? value, +#else + object value, +#endif + int depth = 1) + { + if (value == null) + return "null"; + + var valueAsType = value as Type; + if (valueAsType != null) + return string.Format(CultureInfo.CurrentCulture, "typeof({0})", FormatTypeName(valueAsType, fullTypeName: true)); + + try + { + if (value.GetType().IsEnum) + return FormatEnumValue(value); + + if (value is char c) + return FormatCharValue(c); + + if (value is float) + return FormatFloatValue(value); + + if (value is double) + return FormatDoubleValue(value); + + if (value is DateTime || value is DateTimeOffset) + return FormatDateTimeValue(value); + + if (value is string stringParameter) + return FormatStringValue(stringParameter); + +#if !XUNIT_ARGUMENTFORMATTER_PRIVATE + if (value is CollectionTracker tracker) + return tracker.FormatStart(depth); +#endif + + if (value is IEnumerable enumerable) + return FormatEnumerableValue(enumerable, depth); + + var type = value.GetType(); + +#if NET8_0_OR_GREATER + if (value is ITuple tuple) + return FormatTupleValue(tuple, depth); +#else + if (tupleInterfaceType != null && type.GetInterfaces().Contains(tupleInterfaceType)) + return FormatTupleValue(value, depth); +#endif + + if (type.IsValueType) + return FormatValueTypeValue(value, type); + + if (value is Task task) + { + var typeParameters = type.GenericTypeArguments; + var typeName = + typeParameters.Length == 0 + ? "Task" + : string.Format(CultureInfo.CurrentCulture, "Task<{0}>", string.Join(",", typeParameters.Select(t => FormatTypeName(t)))); + + return string.Format(CultureInfo.CurrentCulture, "{0} {{ Status = {1} }}", typeName, task.Status); + } + + // TODO: ValueTask? + + var isAnonymousType = type.IsAnonymousType(); + return FormatComplexValue(value, depth, type, isAnonymousType); + } + catch (Exception ex) + { + // Sometimes an exception is thrown when formatting an argument, such as in ToString. + // In these cases, we don't want to crash, as tests may have passed despite this. + return string.Format(CultureInfo.CurrentCulture, "{0} was thrown formatting an object of type \"{1}\"", ex.GetType().Name, value.GetType()); + } + } + + static string FormatCharValue(char value) + { + if (value == '\'') + return @"'\''"; + + // Take care of all of the escape sequences + if (TryGetEscapeSequence(value, out var escapeSequence)) + return string.Format(CultureInfo.CurrentCulture, "'{0}'", escapeSequence); + + if (char.IsLetterOrDigit(value) || char.IsPunctuation(value) || char.IsSymbol(value) || value == ' ') + return string.Format(CultureInfo.CurrentCulture, "'{0}'", value); + + // Fallback to hex + return string.Format(CultureInfo.CurrentCulture, "0x{0:x4}", (int)value); + } + + static string FormatDateTimeValue(object value) => + string.Format(CultureInfo.CurrentCulture, "{0:o}", value); + + static string FormatDoubleValue(object value) => + string.Format(CultureInfo.CurrentCulture, "{0:G17}", value); + + static string FormatEnumValue(object value) => +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + value.ToString()?.Replace(", ", " | ", StringComparison.Ordinal) ?? "null"; +#else + value.ToString()?.Replace(", ", " | ") ?? "null"; +#endif + + static string FormatEnumerableValue( + IEnumerable enumerable, + int depth) + { + if (depth > MaxObjectDepth) + return EllipsisInBrackets; + + var result = new StringBuilder(GetGroupingKeyPrefix(enumerable)); + if (result.Length == 0 && !SafeToMultiEnumerate(enumerable)) + return EllipsisInBrackets; + + // This should only be used on values that are known to be re-enumerable + // safely, like collections that implement IDictionary or IList. + var idx = 0; + var enumerator = enumerable.GetEnumerator(); + + result.Append('['); + + while (enumerator.MoveNext()) + { + if (idx != 0) + result.Append(", "); + + if (idx == MaxEnumerableLength) + { + result.Append(Ellipsis); + break; + } + + var current = enumerator.Current; + var nextDepth = current is IEnumerable ? depth + 1 : depth; + + result.Append(Format(current, nextDepth)); + + ++idx; + } + + result.Append(']'); + return result.ToString(); + } + + static string FormatFloatValue(object value) => + string.Format(CultureInfo.CurrentCulture, "{0:G9}", value); + + static string FormatStringValue(string value) + { +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + value = EscapeString(value).Replace(@"""", @"\""", StringComparison.Ordinal); // escape double quotes +#else + value = EscapeString(value).Replace(@"""", @"\"""); // escape double quotes +#endif + + if (value.Length > MaxStringLength) + { + var displayed = value.Substring(0, MaxStringLength); + return string.Format(CultureInfo.CurrentCulture, "\"{0}\"{1}", displayed, Ellipsis); + } + + return string.Format(CultureInfo.CurrentCulture, "\"{0}\"", value); + } + + static string FormatTupleValue( +#if NET8_0_OR_GREATER + ITuple tupleParameter, +#else + object tupleParameter, +#endif + int depth) + { + var result = new StringBuilder("Tuple ("); +#if NET8_0_OR_GREATER + var length = tupleParameter.Length; +#elif XUNIT_NULLABLE + var length = (int)tupleLength!.GetValue(tupleParameter)!; +#else + var length = (int)tupleLength.GetValue(tupleParameter); +#endif + + for (var idx = 0; idx < length; ++idx) + { + if (idx != 0) + result.Append(", "); + +#if NET8_0_OR_GREATER + var value = tupleParameter[idx]; +#elif XUNIT_NULLABLE + var value = tupleIndexer!.GetValue(tupleParameter, new object[] { idx }); +#else + var value = tupleIndexer.GetValue(tupleParameter, new object[] { idx }); +#endif + result.Append(Format(value, depth + 1)); + } + + result.Append(')'); + + return result.ToString(); + } + + /// + /// Formats a type. This maps built-in C# types to their C# native name (e.g., printing "int" instead + /// of "Int32" or "System.Int32"). + /// + /// The type to get the formatted name of + /// Set to to include the namespace; set to for just the simple type name + public static string FormatTypeName( + Type type, + bool fullTypeName = false) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(type, nameof(type)); +#else + if (type is null) + throw new ArgumentNullException(nameof(type)); +#endif + + var arraySuffix = ""; + + // Deconstruct and re-construct array + while (type.IsArray) + { + if (type.IsSZArrayType()) + arraySuffix += "[]"; + else + { + var rank = type.GetArrayRank(); + if (rank == 1) + arraySuffix += "[*]"; + else + arraySuffix += string.Format(CultureInfo.CurrentCulture, "[{0}]", new string(',', rank - 1)); + } + +#if XUNIT_NULLABLE + type = type.GetElementType()!; +#else + type = type.GetElementType(); +#endif + } + + // Map C# built-in type names + var shortType = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + if (!TypeMappings.TryGetValue(shortType, out var result)) + result = fullTypeName ? type.FullName : type.Name; + + if (result is null) + return type.Name; + +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + var tickIdx = result.IndexOf('`', StringComparison.Ordinal); +#else + var tickIdx = result.IndexOf('`'); +#endif + if (tickIdx > 0) + result = result.Substring(0, tickIdx); + + if (type.IsGenericTypeDefinition) + result = string.Format(CultureInfo.CurrentCulture, "{0}<{1}>", result, new string(',', type.GetGenericArguments().Length - 1)); + else if (type.IsGenericType) + { + if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) + result = FormatTypeName(type.GenericTypeArguments[0]) + "?"; + else + result = string.Format(CultureInfo.CurrentCulture, "{0}<{1}>", result, string.Join(", ", type.GenericTypeArguments.Select(t => FormatTypeName(t)))); + } + + return result + arraySuffix; + } + + static int GetEnvironmentValue( + string environmentVariableName, + int defaultValue, + bool allowMaxValue = true) + { + var stringValue = Environment.GetEnvironmentVariable(environmentVariableName); + if (string.IsNullOrWhiteSpace(stringValue) || !int.TryParse(stringValue, out var intValue)) + return defaultValue; + + if (intValue <= 0) + return allowMaxValue ? int.MaxValue : defaultValue; + + return intValue; + } + + static bool IsAnonymousType(this Type type) + { + // There isn't a sanctioned way to do this, so we look for compiler-generated types that + // include "AnonymousType" in their names. + if (type.GetCustomAttribute() == null) + return false; + +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return type.Name.Contains("AnonymousType", StringComparison.Ordinal); +#else + return type.Name.Contains("AnonymousType"); +#endif + } + + static bool IsSZArrayType(this Type type) => +#if NET8_0_OR_GREATER + type.IsSZArray; +#elif XUNIT_NULLABLE + type == type.GetElementType()!.MakeArrayType(); +#else + type == type.GetElementType().MakeArrayType(); +#endif + + static bool SafeToMultiEnumerate(IEnumerable collection) => + collection is Array || + collection is BitArray || + collection is IList || + collection is IDictionary || + GetSetElementType(collection) != null || + IsEnumerableOfGrouping(collection); + + static bool TryGetEscapeSequence( + char ch, +#if XUNIT_NULLABLE + out string? value) +#else + out string value) +#endif + { + value = null; + + if (ch == '\t') // tab + value = @"\t"; + if (ch == '\n') // newline + value = @"\n"; + if (ch == '\v') // vertical tab + value = @"\v"; + if (ch == '\a') // alert + value = @"\a"; + if (ch == '\r') // carriage return + value = @"\r"; + if (ch == '\f') // formfeed + value = @"\f"; + if (ch == '\b') // backspace + value = @"\b"; + if (ch == '\0') // null char + value = @"\0"; + if (ch == '\\') // backslash + value = @"\\"; + + return value != null; + } + +#if XUNIT_NULLABLE + internal static Exception? UnwrapException(Exception? ex) +#else + internal static Exception UnwrapException(Exception ex) +#endif + { + if (ex == null) + return null; + + while (true) + { + var tiex = ex as TargetInvocationException; + if (tiex == null || tiex.InnerException == null) + return ex; + + ex = tiex.InnerException; + } + } + + static string WrapAndGetFormattedValue( +#if XUNIT_NULLABLE + Func getter, +#else + Func getter, +#endif + int depth) + { + try + { + return Format(getter(), depth + 1); + } + catch (Exception ex) + { + return string.Format(CultureInfo.CurrentCulture, "(throws {0})", UnwrapException(ex)?.GetType().Name); + } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter_aot.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter_aot.cs new file mode 100644 index 00000000000..369d05b213a --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter_aot.cs @@ -0,0 +1,76 @@ +#if XUNIT_AOT + +#pragma warning disable IDE0060 // Remove unused parameter + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; + +#if XUNIT_ARGUMENTFORMATTER_PRIVATE +namespace Xunit.Internal +#else +namespace Xunit.Sdk +#endif +{ + partial class ArgumentFormatter + { + /// + /// Formats a value for display. + /// + /// The value to be formatted + public static string Format(KeyValuePair value) => + string.Format( + CultureInfo.CurrentCulture, + "[{0}] = {1}", + Format(value.Key), + Format(value.Value) + ); + + static string FormatComplexValue( + object value, + int depth, + Type type, + bool isAnonymousType) + { + // For objects which implement a custom ToString method, just call that + var toString = value.ToString(); + if (toString is string && toString != type.FullName) + return toString; + + return string.Format(CultureInfo.CurrentCulture, "{0}{{ {1} }}", isAnonymousType ? "" : type.Name + " ", Ellipsis); + } + + static string FormatValueTypeValue( + object value, + Type type) => + Convert.ToString(value, CultureInfo.CurrentCulture) ?? "null"; + +#if XUNIT_NULLABLE + static string? GetGroupingKeyPrefix(IEnumerable enumerable) => +#else + static string GetGroupingKeyPrefix(IEnumerable enumerable) => +#endif + null; + +#if XUNIT_NULLABLE + internal static Type? GetSetElementType(object? obj) => +#else + internal static Type GetSetElementType(object obj) => +#endif + null; + + static bool IsEnumerableOfGrouping(IEnumerable collection) => + false; + } +} + +#endif // XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter_reflection.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter_reflection.cs new file mode 100644 index 00000000000..19232771458 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/ArgumentFormatter_reflection.cs @@ -0,0 +1,198 @@ +#if !XUNIT_AOT + +#pragma warning disable IDE0300 // Simplify collection initialization +#pragma warning disable IDE0301 // Simplify collection initialization +#pragma warning disable IDE0305 // Simplify collection initialization +#pragma warning disable CA1810 // Initialize reference type static fields inline + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8600 +#pragma warning disable CS8601 +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#pragma warning disable CS8618 +#pragma warning disable CS8625 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; + +#if XUNIT_ARGUMENTFORMATTER_PRIVATE +namespace Xunit.Internal +#else +namespace Xunit.Sdk +#endif +{ + partial class ArgumentFormatter + { + static readonly object[] EmptyObjects = Array.Empty(); + static readonly Type[] EmptyTypes = Array.Empty(); + +#if !NET8_0_OR_GREATER + +#if XUNIT_NULLABLE + static readonly PropertyInfo? tupleIndexer; + static readonly Type? tupleInterfaceType; + static readonly PropertyInfo? tupleLength; +#else + static readonly PropertyInfo tupleIndexer; + static readonly Type tupleInterfaceType; + static readonly PropertyInfo tupleLength; +#endif + + static ArgumentFormatter() + { + tupleInterfaceType = Type.GetType("System.Runtime.CompilerServices.ITuple"); + + if (tupleInterfaceType != null) + { + tupleIndexer = tupleInterfaceType.GetRuntimeProperty("Item"); + tupleLength = tupleInterfaceType.GetRuntimeProperty("Length"); + } + + if (tupleIndexer == null || tupleLength == null) + tupleInterfaceType = null; + } + +#endif // !NET8_0_OR_GREATER + + static string FormatComplexValue( + object value, + int depth, + Type type, + bool isAnonymousType) + { + // For objects which implement a custom ToString method, just call that + if (!isAnonymousType) + { + var toString = type.GetRuntimeMethod("ToString", EmptyTypes); + if (toString != null && toString.DeclaringType != typeof(object)) + return toString.Invoke(value, EmptyObjects) as string ?? "null"; + } + + var typeName = isAnonymousType ? "" : type.Name + " "; + + if (depth > MaxObjectDepth) + return string.Format(CultureInfo.CurrentCulture, "{0}{{ {1} }}", typeName, Ellipsis); + + var fields = + type + .GetRuntimeFields() + .Where(f => f.IsPublic && !f.IsStatic) + .Select(f => new { name = f.Name, value = WrapAndGetFormattedValue(() => f.GetValue(value), depth + 1) }); + + var properties = + type + .GetRuntimeProperties() + .Where(p => p.GetMethod != null && p.GetMethod.IsPublic && !p.GetMethod.IsStatic) + .Select(p => new { name = p.Name, value = WrapAndGetFormattedValue(() => p.GetValue(value), depth + 1) }); + + var parameters = + MaxObjectMemberCount == int.MaxValue + ? fields.Concat(properties).OrderBy(p => p.name).ToList() + : fields.Concat(properties).OrderBy(p => p.name).Take(MaxObjectMemberCount + 1).ToList(); + + if (parameters.Count == 0) + return string.Format(CultureInfo.CurrentCulture, "{0}{{ }}", typeName); + + var formattedParameters = string.Join(", ", parameters.Take(MaxObjectMemberCount).Select(p => string.Format(CultureInfo.CurrentCulture, "{0} = {1}", p.name, p.value))); + + if (parameters.Count > MaxObjectMemberCount) + formattedParameters += ", " + Ellipsis; + + return string.Format(CultureInfo.CurrentCulture, "{0}{{ {1} }}", typeName, formattedParameters); + } + + static string FormatValueTypeValue( + object value, + Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) + { + var k = type.GetProperty("Key")?.GetValue(value, null); + var v = type.GetProperty("Value")?.GetValue(value, null); + + return string.Format(CultureInfo.CurrentCulture, "[{0}] = {1}", Format(k), Format(v)); + } + + return Convert.ToString(value, CultureInfo.CurrentCulture) ?? "null"; + } + +#if XUNIT_NULLABLE + static string? GetGroupingKeyPrefix(IEnumerable enumerable) +#else + static string GetGroupingKeyPrefix(IEnumerable enumerable) +#endif + { + var groupingTypes = GetGroupingTypes(enumerable); + if (groupingTypes == null) + return null; + + var groupingInterface = typeof(IGrouping<,>).MakeGenericType(groupingTypes); + var key = groupingInterface.GetRuntimeProperty("Key")?.GetValue(enumerable); + return string.Format(CultureInfo.CurrentCulture, "[{0}] = ", key?.ToString() ?? "null"); + } + +#if XUNIT_NULLABLE + internal static Type[]? GetGroupingTypes(object? obj) +#else + internal static Type[] GetGroupingTypes(object obj) +#endif + { + if (obj == null) + return null; + + return + (from @interface in obj.GetType().GetInterfaces() + where @interface.IsGenericType + let genericTypeDefinition = @interface.GetGenericTypeDefinition() + where genericTypeDefinition == typeof(IGrouping<,>) + select @interface).FirstOrDefault()?.GenericTypeArguments; + } + +#if XUNIT_NULLABLE + internal static Type? GetSetElementType(object? obj) +#else + internal static Type GetSetElementType(object obj) +#endif + { + if (obj == null) + return null; + + return + (from @interface in obj.GetType().GetInterfaces() + where @interface.IsGenericType + let genericTypeDefinition = @interface.GetGenericTypeDefinition() + where genericTypeDefinition == typeof(ISet<>) + select @interface).FirstOrDefault()?.GenericTypeArguments[0]; + } + + static bool IsEnumerableOfGrouping(IEnumerable collection) + { + var genericEnumerableType = + (from @interface in collection.GetType().GetInterfaces() + where @interface.IsGenericType + let genericTypeDefinition = @interface.GetGenericTypeDefinition() + where genericTypeDefinition == typeof(IEnumerable<>) + select @interface).FirstOrDefault()?.GenericTypeArguments[0]; + + if (genericEnumerableType == null) + return false; + + return + genericEnumerableType + .GetInterfaces() + .Concat(new[] { genericEnumerableType }) + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IGrouping<,>)); + } + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer.cs new file mode 100644 index 00000000000..f9162a1d362 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer.cs @@ -0,0 +1,158 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0063 // Use simple 'using' statement +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0290 // Use primary constructor + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#pragma warning disable CS8625 +#pragma warning disable CS8767 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Sdk +{ + static partial class AssertEqualityComparer + { + /// + /// This exception is thrown when an operation failure has occured during equality comparison operations. + /// This generally indicates that a necessary pre-condition was not met for comparison operations to succeed. + /// + public sealed class OperationalFailureException : Exception + { + OperationalFailureException(string message) : + base(message) + { } + + /// + /// Gets an exception that indicates that GetHashCode was called on + /// which usually indicates that an item comparison function was used to try to compare two hash sets. + /// + public static OperationalFailureException ForIllegalGetHashCode() => + new OperationalFailureException("During comparison of two collections, GetHashCode was called, but only a comparison function was provided. This typically indicates trying to compare two sets with an item comparison function, which is not supported. For more information, see https://xunit.net/docs/hash-sets-vs-linear-containers"); + } + } + + /// + /// Default implementation of used by the assertion library. + /// + /// The type that is being compared. + sealed partial class AssertEqualityComparer : IAssertEqualityComparer + { + internal static readonly IEqualityComparer DefaultInnerComparer = AssertEqualityComparer.GetDefaultInnerComparer(typeof(T)); + + readonly Lazy innerComparer; + + /// + /// Initializes a new instance of the class. + /// + /// The inner comparer to be used when the compared objects are enumerable. +#if XUNIT_NULLABLE + public AssertEqualityComparer(IEqualityComparer? innerComparer = null) +#else + public AssertEqualityComparer(IEqualityComparer innerComparer = null) +#endif + { + // Use a thunk to delay evaluation of DefaultInnerComparer + this.innerComparer = new Lazy(() => innerComparer ?? DefaultInnerComparer); + } + + public IEqualityComparer InnerComparer => + innerComparer.Value; + + /// + public bool Equals( +#if XUNIT_NULLABLE + T? x, + T? y) +#else + T x, + T y) +#endif + { + using (var xTracker = x.AsNonStringTracker()) + using (var yTracker = y.AsNonStringTracker()) + return Equals(x, xTracker, y, yTracker).Equal; + } + +#if XUNIT_NULLABLE + public static IEqualityComparer FromComparer(Func comparer) => +#else + public static IEqualityComparer FromComparer(Func comparer) => +#endif + new FuncEqualityComparer(comparer); + + /// + public int GetHashCode(T obj) => + innerComparer.Value.GetHashCode(GuardArgumentNotNull(nameof(obj), obj)); + + /// +#if XUNIT_NULLABLE + [return: NotNull] +#endif + internal static TArg GuardArgumentNotNull( + string argName, +#if XUNIT_NULLABLE + [NotNull] TArg? argValue) +#else + TArg argValue) +#endif + { + if (argValue == null) + throw new ArgumentNullException(argName.TrimStart('@')); + + return argValue; + } + +#if XUNIT_NULLABLE + sealed class FuncEqualityComparer : IEqualityComparer +#else + sealed class FuncEqualityComparer : IEqualityComparer +#endif + { + readonly Func comparer; + + public FuncEqualityComparer(Func comparer) => + this.comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + + public bool Equals( +#if XUNIT_NULLABLE + T? x, + T? y) +#else + T x, + T y) +#endif + { + if (x == null) + return y == null; + + if (y == null) + return false; + + return comparer(x, y); + } + +#if XUNIT_NULLABLE + public int GetHashCode(T? obj) +#else + public int GetHashCode(T obj) +#endif + { +#pragma warning disable CA1065 // This method should never be called, and this exception is a way to highlight if it does + throw AssertEqualityComparer.OperationalFailureException.ForIllegalGetHashCode(); +#pragma warning restore CA1065 + } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparerAdapter.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparerAdapter.cs new file mode 100644 index 00000000000..a8493033bdc --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparerAdapter.cs @@ -0,0 +1,88 @@ +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable IDE0290 // Use primary constructor + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#pragma warning disable CS8767 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Xunit.Sdk +{ + /// + /// A class that wraps to add . + /// + /// The type that is being compared. + sealed class AssertEqualityComparerAdapter : IEqualityComparer, IAssertEqualityComparer + { + readonly IEqualityComparer innerComparer; + + /// + /// Initializes a new instance of the class. + /// + /// The comparer that is being adapted. + public AssertEqualityComparerAdapter(IEqualityComparer innerComparer) => + this.innerComparer = innerComparer ?? throw new ArgumentNullException(nameof(innerComparer)); + + /// + public new bool Equals( +#if XUNIT_NULLABLE + object? x, + object? y) => + innerComparer.Equals((T?)x, (T?)y); +#else + object x, + object y) => + innerComparer.Equals((T)x, (T)y); +#endif + + /// + public bool Equals( +#if XUNIT_NULLABLE + T? x, + T? y) => +#else + T x, + T y) => +#endif + innerComparer.Equals(x, y); + + public AssertEqualityResult Equals( +#if XUNIT_NULLABLE + T? x, + CollectionTracker? xTracker, + T? y, + CollectionTracker? yTracker) +#else + T x, + CollectionTracker xTracker, + T y, + CollectionTracker yTracker) +#endif + { + if (innerComparer is IAssertEqualityComparer innerAssertEqualityComparer) + return innerAssertEqualityComparer.Equals(x, xTracker, y, yTracker); + + return AssertEqualityResult.ForResult(Equals(x, y), x, y); + } + + /// + public int GetHashCode(object obj) => + innerComparer.GetHashCode((T)obj); + + /// + public int GetHashCode(T obj) => + // This warning disable is here because sometimes IEqualityComparer.GetHashCode marks the obj parameter + // with [DisallowNull] and sometimes it doesn't, and we need to be able to support both scenarios when + // someone brings in the assertion library via source. +#pragma warning disable CS8607 + innerComparer.GetHashCode(obj); +#pragma warning restore CS8607 + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer_aot.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer_aot.cs new file mode 100644 index 00000000000..8d5e48e5911 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer_aot.cs @@ -0,0 +1,116 @@ +#if XUNIT_AOT + +#pragma warning disable CA1031 // Do not catch general exception types + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#pragma warning disable CS8767 +#endif + +using System; +using System.Collections; +using System.Collections.Immutable; + +namespace Xunit.Sdk +{ + partial class AssertEqualityComparer + { + // Create a new instance each call (matching the non-AOT/reflection path behavior) + // rather than caching in a static field. A cached static field causes a circular + // static initialization dependency: the field initializer triggers + // AssertEqualityComparer's static initializer, which reads the field back + // via GetDefaultInnerComparer before it has been assigned. On Mono/WASM, this + // causes DefaultInnerComparer to be permanently null, leading to + // NullReferenceException when comparing value types via IStructuralEquatable. + internal static IEqualityComparer GetDefaultComparer(Type _) => + new AssertEqualityComparerAdapter(new AssertEqualityComparer()); + + internal static IEqualityComparer GetDefaultInnerComparer(Type _) => + new AssertEqualityComparerAdapter(new AssertEqualityComparer()); + } + + partial class AssertEqualityComparer + { + public AssertEqualityResult Equals( +#if XUNIT_NULLABLE + T? x, + CollectionTracker? xTracker, + T? y, + CollectionTracker? yTracker) +#else + T x, + CollectionTracker xTracker, + T y, + CollectionTracker yTracker) +#endif + { + // Null? + if (x == null && y == null) + return AssertEqualityResult.ForResult(true, x, y); + if (x == null || y == null) + return AssertEqualityResult.ForResult(false, x, y); + + // If you point at the same thing, you're equal + if (ReferenceEquals(x, y)) + return AssertEqualityResult.ForResult(true, x, y); + + // We want the inequality indices for strings + if (x is string xString && y is string yString) + return StringAssertEqualityComparer.Equivalent(xString, yString); + + var xType = x.GetType(); + var yType = y.GetType(); + + // ImmutableArray defines IEquatable> in a way that isn't consistent with the + // needs of an assertion library. https://github.com/xunit/xunit/issues/3137 + if (!xType.IsGenericType || xType.GetGenericTypeDefinition() != typeof(ImmutableArray<>)) + { + // Implements IEquatable? + if (x is IEquatable equatable) + return AssertEqualityResult.ForResult(equatable.Equals(y), x, y); + } + + // Special case collections (before IStructuralEquatable because arrays implement that in a way we don't want to call) + if (xTracker != null && yTracker != null) + return CollectionTracker.AreCollectionsEqual(xTracker, yTracker, InnerComparer, InnerComparer == DefaultInnerComparer); + + // Implements IStructuralEquatable? + if (x is IStructuralEquatable structuralEquatable && structuralEquatable.Equals(y, innerComparer.Value)) + return AssertEqualityResult.ForResult(true, x, y); + + // Implements IComparable? + if (x is IComparable comparableGeneric) + try + { + return AssertEqualityResult.ForResult(comparableGeneric.CompareTo(y) == 0, x, y); + } + catch + { + // Some implementations of IComparable.CompareTo throw exceptions in + // certain situations, such as if x can't compare against y. + // If this happens, just swallow up the exception and continue comparing. + } + + // Implements IComparable? + if (x is IComparable comparable) + try + { + return AssertEqualityResult.ForResult(comparable.CompareTo(y) == 0, x, y); + } + catch + { + // Some implementations of IComparable.CompareTo throw exceptions in + // certain situations, such as if x can't compare against y. + // If this happens, just swallow up the exception and continue comparing. + } + + // Last case, rely on object.Equals + return AssertEqualityResult.ForResult(object.Equals(x, y), x, y); + } + } +} + +#endif // XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer_reflection.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer_reflection.cs new file mode 100644 index 00000000000..6cd660fd72d --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityComparer_reflection.cs @@ -0,0 +1,320 @@ +#if !XUNIT_AOT + +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0290 // Use primary constructor +#pragma warning disable IDE0300 // Simplify collection initialization +#pragma warning disable IDE0340 // Use unbound generic type + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8601 +#pragma warning disable CS8604 +#pragma warning disable CS8605 +#pragma warning disable CS8618 +#pragma warning disable CS8625 +#pragma warning disable CS8767 +#endif + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace Xunit.Sdk +{ + partial class AssertEqualityComparer + { + static readonly ConcurrentDictionary cachedDefaultComparers = new ConcurrentDictionary(); + static readonly ConcurrentDictionary cachedDefaultInnerComparers = new ConcurrentDictionary(); +#if XUNIT_NULLABLE + static readonly object?[] singleNullObject = new object?[] { null }; +#else + static readonly object[] singleNullObject = new object[] { null }; +#endif + + /// + /// Gets the default comparer to be used for the provided when a custom one + /// has not been provided. Creates an instance of wrapped + /// by . + /// + /// The type to be compared + internal static IEqualityComparer GetDefaultComparer(Type type) => + cachedDefaultComparers.GetOrAdd(type, itemType => + { + var comparerType = typeof(AssertEqualityComparer<>).MakeGenericType(itemType); + var comparer = + Activator.CreateInstance(comparerType, singleNullObject) + ?? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Could not create instance of AssertEqualityComparer<{0}>", itemType.FullName ?? itemType.Name)); + + var wrapperType = typeof(AssertEqualityComparerAdapter<>).MakeGenericType(itemType); + var result = + Activator.CreateInstance(wrapperType, new object[] { comparer }) as IEqualityComparer + ?? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Could not create instance of AssertEqualityComparerAdapter<{0}>", itemType.FullName ?? itemType.Name)); + + return result; + }); + + /// + /// Gets the default comparer to be used as an inner comparer for the provided + /// when a custom one has not been provided. For non-collections, this defaults to an -based + /// comparer; for collections, this creates an inner comparer based on the item type in the collection. + /// + /// The type to create an inner comparer for + internal static IEqualityComparer GetDefaultInnerComparer(Type type) => + cachedDefaultInnerComparers.GetOrAdd(type, t => + { + var innerType = typeof(object); + + // string is enumerable, but we don't treat it like a collection + if (t != typeof(string)) + { + var enumerableOfT = + t + .GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + + if (enumerableOfT != null) + innerType = enumerableOfT.GenericTypeArguments[0]; + } + + return GetDefaultComparer(innerType); + }); + } + + partial class AssertEqualityComparer + { + static readonly ConcurrentDictionary cacheOfIComparableOfT = new ConcurrentDictionary(); + static readonly ConcurrentDictionary cacheOfIEquatableOfT = new ConcurrentDictionary(); + static readonly Type typeKeyValuePair = typeof(KeyValuePair<,>); + + /// + public AssertEqualityResult Equals( +#if XUNIT_NULLABLE + T? x, + CollectionTracker? xTracker, + T? y, + CollectionTracker? yTracker) +#else + T x, + CollectionTracker xTracker, + T y, + CollectionTracker yTracker) +#endif + { + // Null? + if (x == null && y == null) + return AssertEqualityResult.ForResult(true, x, y); + if (x == null || y == null) + return AssertEqualityResult.ForResult(false, x, y); + + // If you point at the same thing, you're equal + if (ReferenceEquals(x, y)) + return AssertEqualityResult.ForResult(true, x, y); + + // We want the inequality indices for strings + if (x is string xString && y is string yString) + return StringAssertEqualityComparer.Equivalent(xString, yString); + + var xType = x.GetType(); + var yType = y.GetType(); + + // ImmutableArray defines IEquatable> in a way that isn't consistent with the + // needs of an assertion library. https://github.com/xunit/xunit/issues/3137 + if (!xType.IsGenericType || xType.GetGenericTypeDefinition() != typeof(ImmutableArray<>)) + { + // Implements IEquatable? + if (x is IEquatable equatable) + return AssertEqualityResult.ForResult(equatable.Equals(y), x, y); + + // Implements IEquatable? + if (xType != yType) + { + var iequatableY = cacheOfIEquatableOfT.GetOrAdd(yType, (t) => typeof(IEquatable<>).MakeGenericType(t)); + if (iequatableY.IsAssignableFrom(xType)) + { + var equalsMethod = iequatableY.GetMethod(nameof(IEquatable.Equals)); + if (equalsMethod == null) + return AssertEqualityResult.ForResult(false, x, y); + +#if XUNIT_NULLABLE + return AssertEqualityResult.ForResult(equalsMethod.Invoke(x, new object[] { y }) is true, x, y); +#else + return AssertEqualityResult.ForResult((bool)equalsMethod.Invoke(x, new object[] { y }), x, y); +#endif + } + } + } + + // Special case collections (before IStructuralEquatable because arrays implement that in a way we don't want to call) + if (xTracker != null && yTracker != null) + return CollectionTracker.AreCollectionsEqual(xTracker, yTracker, InnerComparer, InnerComparer == DefaultInnerComparer); + + // Implements IStructuralEquatable? + if (x is IStructuralEquatable structuralEquatable && structuralEquatable.Equals(y, new TypeErasedEqualityComparer(innerComparer.Value))) + return AssertEqualityResult.ForResult(true, x, y); + + // Implements IComparable? + if (x is IComparable comparableGeneric) + try + { + return AssertEqualityResult.ForResult(comparableGeneric.CompareTo(y) == 0, x, y); + } + catch + { + // Some implementations of IComparable.CompareTo throw exceptions in + // certain situations, such as if x can't compare against y. + // If this happens, just swallow up the exception and continue comparing. + } + + // Implements IComparable? + if (xType != yType) + { + var icomparableY = cacheOfIComparableOfT.GetOrAdd(yType, (t) => typeof(IComparable<>).MakeGenericType(t)); + if (icomparableY.IsAssignableFrom(xType)) + { + var compareToMethod = icomparableY.GetMethod(nameof(IComparable.CompareTo)); + if (compareToMethod == null) + return AssertEqualityResult.ForResult(false, x, y); + + try + { +#if XUNIT_NULLABLE + return AssertEqualityResult.ForResult(compareToMethod.Invoke(x, new object[] { y }) is 0, x, y); +#else + return AssertEqualityResult.ForResult((int)compareToMethod.Invoke(x, new object[] { y }) == 0, x, y); +#endif + } + catch + { + // Some implementations of IComparable.CompareTo throw exceptions in + // certain situations, such as if x can't compare against y. + // If this happens, just swallow up the exception and continue comparing. + } + } + } + + // Implements IComparable? + if (x is IComparable comparable) + try + { + return AssertEqualityResult.ForResult(comparable.CompareTo(y) == 0, x, y); + } + catch + { + // Some implementations of IComparable.CompareTo throw exceptions in + // certain situations, such as if x can't compare against y. + // If this happens, just swallow up the exception and continue comparing. + } + + // Special case KeyValuePair + if (xType.IsConstructedGenericType && + xType.GetGenericTypeDefinition() == typeKeyValuePair && + yType.IsConstructedGenericType && + yType.GetGenericTypeDefinition() == typeKeyValuePair) + { + var xKey = xType.GetRuntimeProperty("Key")?.GetValue(x); + var yKey = yType.GetRuntimeProperty("Key")?.GetValue(y); + + if (xKey == null) + { + if (yKey != null) + return AssertEqualityResult.ForResult(false, x, y); + } + else + { + var xKeyType = xKey.GetType(); + var yKeyType = yKey?.GetType(); + + var keyComparer = AssertEqualityComparer.GetDefaultComparer(xKeyType == yKeyType ? xKeyType : typeof(object)); + if (!keyComparer.Equals(xKey, yKey)) + return AssertEqualityResult.ForResult(false, x, y); + } + + var xValue = xType.GetRuntimeProperty("Value")?.GetValue(x); + var yValue = yType.GetRuntimeProperty("Value")?.GetValue(y); + + if (xValue == null) + return AssertEqualityResult.ForResult(yValue is null, x, y); + + var xValueType = xValue.GetType(); + var yValueType = yValue?.GetType(); + + var valueComparer = AssertEqualityComparer.GetDefaultComparer(xValueType == yValueType ? xValueType : typeof(object)); + return AssertEqualityResult.ForResult(valueComparer.Equals(xValue, yValue), x, y); + } + + // Last case, rely on object.Equals + return AssertEqualityResult.ForResult(object.Equals(x, y), x, y); + } + + sealed class TypeErasedEqualityComparer : IEqualityComparer + { + readonly IEqualityComparer innerComparer; + + public TypeErasedEqualityComparer(IEqualityComparer innerComparer) + { + this.innerComparer = innerComparer; + } + +#if XUNIT_NULLABLE + static MethodInfo? equalsMethod; +#else + static MethodInfo equalsMethod; +#endif + + public new bool Equals( +#if XUNIT_NULLABLE + object? x, + object? y) +#else + object x, + object y) +#endif + { + if (x == null) + return y == null; + if (y == null) + return false; + + // Delegate checking of whether two objects are equal to AssertEqualityComparer. + // To get the best result out of AssertEqualityComparer, we attempt to specialize the + // comparer for the objects that we are checking. + // If the objects are the same, great! If not, assume they are objects. + // This is more naive than the C# compiler which tries to see if they share any interfaces + // etc. but that's likely overkill here as AssertEqualityComparer is smart enough. + var objectType = x.GetType() == y.GetType() ? x.GetType() : typeof(object); + + // Lazily initialize and cache the EqualsGeneric method. + if (equalsMethod == null) + { + equalsMethod = typeof(TypeErasedEqualityComparer).GetMethod(nameof(EqualsGeneric), BindingFlags.NonPublic | BindingFlags.Instance); + if (equalsMethod == null) + return false; + } + +#if XUNIT_NULLABLE + return equalsMethod.MakeGenericMethod(objectType).Invoke(this, new object[] { x, y }) is true; +#else + return (bool)equalsMethod.MakeGenericMethod(objectType).Invoke(this, new object[] { x, y }); +#endif + } + + bool EqualsGeneric( + U x, + U y) => + new AssertEqualityComparer(innerComparer: innerComparer).Equals(x, y); + + public int GetHashCode(object obj) => + GuardArgumentNotNull(nameof(obj), obj).GetHashCode(); + } + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityResult.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityResult.cs new file mode 100644 index 00000000000..0d6a71872df --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEqualityResult.cs @@ -0,0 +1,271 @@ +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0290 // Use primary constructor + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8601 +#pragma warning disable CS8618 +#pragma warning disable CS8625 +#pragma warning disable CS8765 +#pragma warning disable CS8767 +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Indicates the result of comparing two values for equality. Includes success/failure information, as well + /// as indices where the values differ, if the values are indexed (e.g., collections or strings). + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class AssertEqualityResult : IEquatable + { + AssertEqualityResult( + bool equal, +#if XUNIT_NULLABLE + object? x, + object? y, +#else + object x, + object y, +#endif + int? mismatchIndexX = null, + int? mismatchIndexY = null, +#if XUNIT_NULLABLE + Exception? exception = null, + AssertEqualityResult? innerResult = null) +#else + Exception exception = null, + AssertEqualityResult innerResult = null) +#endif + { + Equal = equal; + X = x; + Y = y; + Exception = exception; + InnerResult = innerResult; + MismatchIndexX = mismatchIndexX; + MismatchIndexY = mismatchIndexY; + } + + /// + /// Returns if the values were equal; , otherwise. + /// + public bool Equal { get; } + + /// + /// Returns the exception that caused the failure, if it was based on an exception. + /// +#if XUNIT_NULLABLE + public Exception? Exception { get; } +#else + public Exception Exception { get; } +#endif + + /// + /// Returns the comparer result for any inner comparison that caused this result + /// to fail; returns if there was no inner comparison. + /// + /// + /// If this value is set, then it generally indicates that this comparison was a + /// failed collection comparison, and the inner result indicates the specific + /// item comparison that caused the failure. + /// +#if XUNIT_NULLABLE + public AssertEqualityResult? InnerResult { get; } +#else + public AssertEqualityResult InnerResult { get; } +#endif + + /// + /// Returns the index of the mismatch for the X value, if the comparison + /// failed on a specific index. + /// + public int? MismatchIndexX { get; } + + /// + /// Returns the index of the mismatch for the Y value, if the comparison + /// failed on a specific index. + /// + public int? MismatchIndexY { get; } + + /// + /// The left-hand value in the comparison + /// +#if XUNIT_NULLABLE + public object? X { get; } +#else + public object X { get; } +#endif + + /// + /// The right-hand value in the comparison + /// +#if XUNIT_NULLABLE + public object? Y { get; } +#else + public object Y { get; } +#endif + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// Returns if the values are equal; , otherwise. +#if XUNIT_NULLABLE + public override bool Equals(object? obj) => +#else + public override bool Equals(object obj) => +#endif + obj is AssertEqualityResult other && Equals(other); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// Returns if the values are equal; , otherwise. +#if XUNIT_NULLABLE + public bool Equals(AssertEqualityResult? other) +#else + public bool Equals(AssertEqualityResult other) +#endif + { + if (other is null) + return false; + + return + Equal.Equals(other) && + X?.Equals(other.X) != false && + Y?.Equals(other.Y) != false && + InnerResult?.Equals(other.InnerResult) != false && + MismatchIndexX.Equals(other.MismatchIndexY) && + MismatchIndexY.Equals(other.MismatchIndexY); + } + + /// + /// Creates an instance of where the values were + /// not equal, and there is a single mismatch index (for example, when comparing two + /// collections). + /// + /// The left-hand value in the comparison + /// The right-hand value in the comparison + /// The mismatch index for both X and Y values + /// The optional exception that was thrown to cause the failure + /// The optional inner result that caused the equality failure + public static AssertEqualityResult ForMismatch( +#if XUNIT_NULLABLE + object? x, + object? y, +#else + object x, + object y, +#endif + int mismatchIndex, +#if XUNIT_NULLABLE + Exception? exception = null, + AssertEqualityResult? innerResult = null) => +#else + Exception exception = null, + AssertEqualityResult innerResult = null) => +#endif + new AssertEqualityResult(false, x, y, mismatchIndex, mismatchIndex, exception, innerResult); + + /// + /// Creates an instance of where the values were + /// not equal, and there are separate mismatch indices (for example, when comparing two + /// strings under special circumstances). + /// + /// The left-hand value in the comparison + /// The right-hand value in the comparison + /// The mismatch index for the X value + /// The mismatch index for the Y value + /// The optional exception that was thrown to cause the failure + /// The optional inner result that caused the equality failure + public static AssertEqualityResult ForMismatch( +#if XUNIT_NULLABLE + object? x, + object? y, +#else + object x, + object y, +#endif + int mismatchIndexX, + int mismatchIndexY, +#if XUNIT_NULLABLE + Exception? exception = null, + AssertEqualityResult? innerResult = null) => +#else + Exception exception = null, + AssertEqualityResult innerResult = null) => +#endif + new AssertEqualityResult(false, x, y, mismatchIndexX, mismatchIndexY, exception, innerResult); + + /// + /// Creates an instance of . + /// + /// A flag which indicates whether the values were equal + /// The left-hand value in the comparison + /// The right-hand value in the comparison + /// The optional exception that was thrown to cause the failure + /// The optional inner result that caused the equality failure + public static AssertEqualityResult ForResult( + bool equal, +#if XUNIT_NULLABLE + object? x, + object? y, + Exception? exception = null, + AssertEqualityResult? innerResult = null) => +#else + object x, + object y, + Exception exception = null, + AssertEqualityResult innerResult = null) => +#endif + new AssertEqualityResult(equal, x, y, exception: exception, innerResult: innerResult); + + /// + /// Gets a hash code for the object, to be used in hashed containers. + /// + public override int GetHashCode() => + (Equal, MismatchIndexX, MismatchIndexY).GetHashCode(); + + /// + /// Determines whether two instances of are equal. + /// + /// The first value + /// The second value + /// Returns if the values are equal; , otherwise. + public static bool operator ==( +#if XUNIT_NULLABLE + AssertEqualityResult? left, + AssertEqualityResult? right) => +#else + AssertEqualityResult left, + AssertEqualityResult right) => +#endif + left?.Equals(right) == true; + + /// + /// Determines whether two instances of are not equal. + /// + /// The first value + /// The second value + /// Returns if the values are not equal; , otherwise. + public static bool operator !=( +#if XUNIT_NULLABLE + AssertEqualityResult? left, + AssertEqualityResult? right) => +#else + AssertEqualityResult left, + AssertEqualityResult right) => +#endif + left?.Equals(right) == false; + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEquivalenceComparer.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEquivalenceComparer.cs new file mode 100644 index 00000000000..10bdd63f2c0 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertEquivalenceComparer.cs @@ -0,0 +1,104 @@ +#if !XUNIT_AOT + +#pragma warning disable IDE0290 // Use primary constructor + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#pragma warning disable CS8767 +#endif + +using System.Collections; +using System.Collections.Generic; + +namespace Xunit +{ + /// + /// An implementation of that uses the same logic + /// from . + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class AssertEquivalenceComparer : IEqualityComparer + { + readonly bool strict; + + /// + /// Initializes a new instance of the class. + /// + /// A flag indicating whether comparisons should be strict. + public AssertEquivalenceComparer(bool strict) => + this.strict = strict; + + /// + public new bool Equals( +#if XUNIT_NULLABLE + object? x, + object? y) +#else + object x, + object y) +#endif + { + Assert.Equivalent(x, y, strict); + return true; + } + + /// + public int GetHashCode(object obj) => + obj?.GetHashCode() ?? 0; + } + + /// + /// An implementation of that uses the same logic + /// from . + /// + /// The item type being compared + /// + /// A generic version of this is provided so that it can be used with + /// + /// to ensure strict ordering of collections while doing equivalence comparisons for + /// the items inside the collection, per . + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class AssertEquivalenceComparer : IEqualityComparer + { + readonly bool strict; + + /// + /// Initializes a new instance of the class. + /// + /// A flag indicating whether comparisons should be strict. + public AssertEquivalenceComparer(bool strict) => + this.strict = strict; + + /// + public bool Equals( +#if XUNIT_NULLABLE + T? x, + T? y) +#else + T x, + T y) +#endif + { + Assert.Equivalent(x, y, strict); + return true; + } + + /// + public int GetHashCode(T obj) => + obj?.GetHashCode() ?? 0; + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertHelper.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertHelper.cs new file mode 100644 index 00000000000..82d4f5c9918 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertHelper.cs @@ -0,0 +1,314 @@ +#pragma warning disable IDE0057 // Use range operator +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0305 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8625 +#pragma warning disable CS8767 +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +#if NET8_0_OR_GREATER +using System.Threading.Tasks; +#endif + +namespace Xunit.Internal +{ + /// + /// INTERNAL CLASS. DO NOT USE. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + static partial class AssertHelper + { + static readonly Dictionary encodings = new Dictionary + { + { '\0', @"\0" }, // Null + { '\a', @"\a" }, // Alert + { '\b', @"\b" }, // Backspace + { '\f', @"\f" }, // Form feed + { '\n', @"\n" }, // New line + { '\r', @"\r" }, // Carriage return + { '\t', @"\t" }, // Horizontal tab + { '\v', @"\v" }, // Vertical tab + { '\\', @"\\" }, // Backslash + }; + + internal static (int start, int end) GetStartEndForString( +#if XUNIT_NULLABLE + string? value, +#else + string value, +#endif + int index) + { + if (value is null) + return (0, 0); + + if (ArgumentFormatter.MaxStringLength == int.MaxValue) + return (0, value.Length); + + var halfMaxLength = ArgumentFormatter.MaxStringLength / 2; + var start = Math.Max(index - halfMaxLength, 0); + var end = Math.Min(start + ArgumentFormatter.MaxStringLength, value.Length); + start = Math.Max(end - ArgumentFormatter.MaxStringLength, 0); + + return (start, end); + } + + internal static bool IsCompilerGenerated(Type type) => + type.CustomAttributes.Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); + + /// + public static IReadOnlyList<(string Prefix, string Member)> ParseExclusionExpressions(params string[] exclusionExpressions) + { + var result = new List<(string Prefix, string Member)>(); + + foreach (var expression in exclusionExpressions ?? throw new ArgumentNullException(nameof(exclusionExpressions))) + { + if (expression is null || expression.Length is 0) + throw new ArgumentException("Null/empty expressions are not valid.", nameof(exclusionExpressions)); + + var lastDotIdx = expression.LastIndexOf('.'); + if (lastDotIdx == 0) + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + "Expression '{0}' is not valid. Expressions may not start with a period.", + expression + ), + nameof(exclusionExpressions) + ); + + if (lastDotIdx == expression.Length - 1) + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + "Expression '{0}' is not valid. Expressions may not end with a period.", + expression + ), + nameof(exclusionExpressions) + ); + + if (lastDotIdx < 0) + result.Add((string.Empty, expression)); + else + result.Add((expression.Substring(0, lastDotIdx), expression.Substring(lastDotIdx + 1))); + } + + return result; + } + + /// + public static IReadOnlyList<(string Prefix, string Member)> ParseExclusionExpressions(params LambdaExpression[] exclusionExpressions) + { + var result = new List<(string Prefix, string Member)>(); + + foreach (var expression in exclusionExpressions ?? throw new ArgumentNullException(nameof(exclusionExpressions))) + { + if (expression is null) + throw new ArgumentException("Null expression is not valid.", nameof(exclusionExpressions)); + + var memberExpression = default(MemberExpression); + + // The incoming expressions are T => object?, so any boxed struct starts with a conversion + if (expression.Body.NodeType == ExpressionType.Convert && expression.Body is UnaryExpression unaryExpression) + memberExpression = unaryExpression.Operand as MemberExpression; + else + memberExpression = expression.Body as MemberExpression; + + if (memberExpression is null) + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + "Expression '{0}' is not supported. Only property or field expressions from the lambda parameter are supported.", + expression + ), + nameof(exclusionExpressions) + ); + + var pieces = new LinkedList(); + + while (true) + { + pieces.AddFirst(memberExpression.Member.Name); + + if (memberExpression.Expression?.NodeType == ExpressionType.Parameter) + break; + + memberExpression = memberExpression.Expression as MemberExpression; + + if (memberExpression is null) + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + "Expression '{0}' is not supported. Only property or field expressions from the lambda parameter are supported.", + expression + ), + nameof(exclusionExpressions) + ); + } + + if (pieces.Last is null) + continue; + + var member = pieces.Last.Value; + pieces.RemoveLast(); + + var prefix = string.Join(".", pieces.ToArray()); + result.Add((prefix, member)); + } + + return result; + } + + internal static string ShortenAndEncodeString( +#if XUNIT_NULLABLE + string? value, +#else + string value, +#endif + int index, + out int pointerIndent) + { + var (start, end) = GetStartEndForString(value, index); + + return ShortenString(value, start, end, index, out pointerIndent); + } + +#if XUNIT_NULLABLE + internal static string ShortenAndEncodeString(string? value) => +#else + internal static string ShortenAndEncodeString(string value) => +#endif + ShortenAndEncodeString(value, 0, out var _); + +#if XUNIT_NULLABLE + internal static string ShortenAndEncodeStringEnd(string? value) => +#else + internal static string ShortenAndEncodeStringEnd(string value) => +#endif + ShortenAndEncodeString(value, (value?.Length - 1) ?? 0, out var _); + + internal static string ShortenString( +#if XUNIT_NULLABLE + string? value, +#else + string value, +#endif + int start, + int end, + int index, + out int pointerIndent) + { + if (value == null) + { + pointerIndent = -1; + return "null"; + } + + // Set the initial buffer to include the possibility of quotes and ellipses, plus a few extra + // characters for encoding before needing reallocation. + var printedValue = new StringBuilder(end - start + 10); + pointerIndent = 0; + + if (start > 0) + { + printedValue.Append(ArgumentFormatter.Ellipsis); + pointerIndent += 3; + } + + printedValue.Append('\"'); + pointerIndent++; + + for (var idx = start; idx < end; ++idx) + { + var c = value[idx]; + var paddingLength = 1; + + if (encodings.TryGetValue(c, out var encoding)) + { + printedValue.Append(encoding); + paddingLength = encoding.Length; + } + else + printedValue.Append(c); + + if (idx < index) + pointerIndent += paddingLength; + } + + printedValue.Append('\"'); + + if (end < value.Length) + printedValue.Append(ArgumentFormatter.Ellipsis); + + return printedValue.ToString(); + } + +#if NET8_0_OR_GREATER + +#if XUNIT_NULLABLE + [return: NotNullIfNotNull(nameof(data))] + internal static IEnumerable? ToEnumerable(IAsyncEnumerable? data) => +#else + internal static IEnumerable ToEnumerable(IAsyncEnumerable data) => +#endif + data == null ? null : ToEnumerableImpl(data); + + static IEnumerable ToEnumerableImpl(IAsyncEnumerable data) + { + var enumerator = data.GetAsyncEnumerator(); + + try + { + while (WaitForValueTask(enumerator.MoveNextAsync())) + yield return enumerator.Current; + } + finally + { + WaitForValueTask(enumerator.DisposeAsync()); + } + } + + static void WaitForValueTask(ValueTask valueTask) + { + var valueTaskAwaiter = valueTask.GetAwaiter(); + if (valueTaskAwaiter.IsCompleted) + return; + + // Let the task complete on a thread pool thread while we block the main thread + Task.Run(valueTask.AsTask).GetAwaiter().GetResult(); + } + + static T WaitForValueTask(ValueTask valueTask) + { + var valueTaskAwaiter = valueTask.GetAwaiter(); + if (valueTaskAwaiter.IsCompleted) + return valueTaskAwaiter.GetResult(); + + // Let the task complete on a thread pool thread while we block the main thread + return Task.Run(valueTask.AsTask).GetAwaiter().GetResult(); + } + +#endif // NET8_0_OR_GREATER + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertHelper_reflection.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertHelper_reflection.cs new file mode 100644 index 00000000000..5cf3531ca93 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertHelper_reflection.cs @@ -0,0 +1,576 @@ +#if !XUNIT_AOT + +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0300 // Collection initialization can be simplified +#pragma warning disable IDE0301 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#pragma warning disable CS8621 +#pragma warning disable CS8625 +#pragma warning disable CS8767 +#endif + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Internal +{ + partial class AssertHelper + { +#if XUNIT_NULLABLE + static readonly Lazy fileSystemInfoType = new Lazy(() => GetTypeByName("System.IO.FileSystemInfo")); + static readonly Lazy fileSystemInfoFullNameProperty = new Lazy(() => fileSystemInfoType.Value?.GetProperty("FullName")); + static readonly ConcurrentDictionary>> gettersByType = new ConcurrentDictionary>>(); +#else + static readonly Lazy fileSystemInfoType = new Lazy(() => GetTypeByName("System.IO.FileSystemInfo")); + static readonly Lazy fileSystemInfoFullNameProperty = new Lazy(() => fileSystemInfoType.Value?.GetProperty("FullName")); + static readonly ConcurrentDictionary>> gettersByType = new ConcurrentDictionary>>(); +#endif + + static readonly IReadOnlyList<(string Prefix, string Member)> emptyExclusions = Array.Empty<(string Prefix, string Member)>(); + static readonly Lazy getAssemblies = new Lazy(AppDomain.CurrentDomain.GetAssemblies); + static readonly Lazy maxCompareDepth = new Lazy(() => + { + var stringValue = Environment.GetEnvironmentVariable(EnvironmentVariables.AssertEquivalentMaxDepth); + if (stringValue is null || !int.TryParse(stringValue, out var intValue) || intValue <= 0) + return EnvironmentVariables.Defaults.AssertEquivalentMaxDepth; + return intValue; + }); + static readonly Type objectType = typeof(object); + static readonly IEqualityComparer referenceEqualityComparer = new ReferenceEqualityComparer(); + +#if XUNIT_NULLABLE + static Dictionary> GetGettersForType(Type type) => +#else + static Dictionary> GetGettersForType(Type type) => +#endif + gettersByType.GetOrAdd(type, _type => + { + var fieldGetters = + _type + .GetRuntimeFields() + .Where(f => f.IsPublic && !f.IsStatic) +#if XUNIT_NULLABLE + .Select(f => new { name = f.Name, getter = (Func)f.GetValue }); +#else + .Select(f => new { name = f.Name, getter = (Func)f.GetValue }); +#endif + + var propertyGetters = + _type + .GetRuntimeProperties() + .Where(p => + p.CanRead + && p.GetMethod != null + && p.GetMethod.IsPublic + && !p.GetMethod.IsStatic +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + && !p.GetMethod.ReturnType.IsByRefLike +#endif + && p.GetIndexParameters().Length == 0 + && !p.GetCustomAttributes().Any() + && !p.GetMethod.GetCustomAttributes().Any() + ) + .GroupBy(p => p.Name) + .Select(group => + { + // When there is more than one property with the same name, we take the one from + // the most derived class. Start assuming the first one is the correct one, and then + // visit each in turn to see whether it's more derived or not. + var targetProperty = group.First(); + + foreach (var candidateProperty in group.Skip(1)) + for (var candidateType = candidateProperty.DeclaringType?.BaseType; candidateType != null; candidateType = candidateType.BaseType) + if (targetProperty.DeclaringType == candidateType) + { + targetProperty = candidateProperty; + break; + } + +#if XUNIT_NULLABLE + return new { name = targetProperty.Name, getter = (Func)targetProperty.GetValue }; +#else + return new { name = targetProperty.Name, getter = (Func)targetProperty.GetValue }; +#endif + }); + + return + fieldGetters + .Concat(propertyGetters) + .ToDictionary(g => g.name, g => g.getter); + }); + +#if XUNIT_NULLABLE + static Type? GetTypeByName(string typeName) +#else + static Type GetTypeByName(string typeName) +#endif + { + try + { + foreach (var assembly in getAssemblies.Value) + { + var type = assembly.GetType(typeName); + if (type != null) + return type; + } + + return null; + } + catch (Exception ex) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Fatal error: Exception occurred while trying to retrieve type '{0}'", typeName), ex); + } + } + + static bool TryConvert( + object value, + Type targetType, +#if XUNIT_NULLABLE + [NotNullWhen(true)] out object? converted) +#else + out object converted) +#endif + { + try + { + converted = Convert.ChangeType(value, targetType, CultureInfo.CurrentCulture); + return converted != null; + } + catch (InvalidCastException) + { + converted = null; + return false; + } + } + +#if XUNIT_NULLABLE + static object? UnwrapLazy( + object? value, +#else + static object UnwrapLazy( + object value, +#endif + out Type valueType) + { + if (value == null) + { + valueType = objectType; + + return null; + } + + valueType = value.GetType(); + + if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Lazy<>)) + { + var property = valueType.GetRuntimeProperty("Value"); + if (property != null) + { + valueType = valueType.GenericTypeArguments[0]; + return property.GetValue(value); + } + } + + return value; + } + + /// +#if XUNIT_NULLABLE + public static EquivalentException? VerifyEquivalence( + object? expected, + object? actual, +#else + public static EquivalentException VerifyEquivalence( + object expected, + object actual, +#endif + bool strict, +#if XUNIT_NULLABLE + IReadOnlyList<(string Prefix, string Member)>? exclusions = null) => +#else + IReadOnlyList<(string Prefix, string Member)> exclusions = null) => +#endif + VerifyEquivalence( + expected, + actual, + strict, + string.Empty, + new HashSet(referenceEqualityComparer), + new HashSet(referenceEqualityComparer), + 1, + exclusions ?? emptyExclusions + ); + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalence( + object? expected, + object? actual, +#else + static EquivalentException VerifyEquivalence( + object expected, + object actual, +#endif + bool strict, + string prefix, + HashSet expectedRefs, + HashSet actualRefs, + int depth, + IReadOnlyList<(string Prefix, string Member)> exclusions) + { + // Check for exceeded depth + if (depth > maxCompareDepth.Value) + return EquivalentException.ForExceededDepth(maxCompareDepth.Value, prefix); + + // Unwrap Lazy + expected = UnwrapLazy(expected, out var expectedType); + actual = UnwrapLazy(actual, out var actualType); + + // Check for null equivalence + if (expected == null) + return + actual == null + ? null + : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + + if (actual == null) + return EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + + // Check for identical references + if (ReferenceEquals(expected, actual)) + return null; + + // Prevent circular references + if (expectedRefs.Contains(expected)) + return EquivalentException.ForCircularReference(string.Format(CultureInfo.CurrentCulture, "{0}.{1}", nameof(expected), prefix)); + + if (actualRefs.Contains(actual)) + return EquivalentException.ForCircularReference(string.Format(CultureInfo.CurrentCulture, "{0}.{1}", nameof(actual), prefix)); + + try + { + expectedRefs.Add(expected); + actualRefs.Add(actual); + + // Primitive types, enums and strings should just fall back to their Equals implementation + if (expectedType.IsPrimitive || expectedType.IsEnum || expectedType == typeof(string) || expectedType == typeof(decimal) || expectedType == typeof(Guid)) + return VerifyEquivalenceIntrinsics(expected, actual, prefix); + + // DateTime and DateTimeOffset need to be compared via IComparable (because of a circular + // reference via the Date property). + if (expectedType == typeof(DateTime) || expectedType == typeof(DateTimeOffset)) + return VerifyEquivalenceDateTime(expected, actual, prefix); + + // FileSystemInfo has a recursion problem when getting the root directory + if (fileSystemInfoType.Value != null) + if (fileSystemInfoType.Value.IsAssignableFrom(expectedType) && fileSystemInfoType.Value.IsAssignableFrom(actualType)) + return VerifyEquivalenceFileSystemInfo(expected, actual, strict, prefix, expectedRefs, actualRefs, depth, exclusions); + + // Uri can throw for relative URIs + var expectedUri = expected as Uri; + var actualUri = actual as Uri; + if (expectedUri != null && actualUri != null) + return VerifyEquivalenceUri(expectedUri, actualUri, prefix); + + // IGrouping is special, since it implements IEnumerable + var expectedGroupingTypes = ArgumentFormatter.GetGroupingTypes(expected); + if (expectedGroupingTypes != null) + { + var actualGroupingTypes = ArgumentFormatter.GetGroupingTypes(actual); + if (actualGroupingTypes != null) + return VerifyEquivalenceGroupings(expected, expectedGroupingTypes, actual, actualGroupingTypes, strict); + } + + // Enumerables? Check equivalence of individual members + if (expected is IEnumerable enumerableExpected && actual is IEnumerable enumerableActual) + return VerifyEquivalenceEnumerable(enumerableExpected, enumerableActual, strict, prefix, expectedRefs, actualRefs, depth, exclusions); + + return VerifyEquivalenceReference(expected, actual, strict, prefix, expectedRefs, actualRefs, depth, exclusions); + } + finally + { + expectedRefs.Remove(expected); + actualRefs.Remove(actual); + } + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceDateTime( +#else + static EquivalentException VerifyEquivalenceDateTime( +#endif + object expected, + object actual, + string prefix) + { + try + { + if (expected is IComparable expectedComparable) + return + expectedComparable.CompareTo(actual) == 0 + ? null + : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + } + catch (Exception ex) + { + return EquivalentException.ForMemberValueMismatch(expected, actual, prefix, ex); + } + + try + { + if (actual is IComparable actualComparable) + return + actualComparable.CompareTo(expected) == 0 + ? null + : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + } + catch (Exception ex) + { + return EquivalentException.ForMemberValueMismatch(expected, actual, prefix, ex); + } + + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "VerifyEquivalenceDateTime was given non-DateTime(Offset) objects; typeof(expected) = {0}, typeof(actual) = {1}", + ArgumentFormatter.FormatTypeName(expected.GetType()), + ArgumentFormatter.FormatTypeName(actual.GetType()) + ) + ); + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceEnumerable( +#else + static EquivalentException VerifyEquivalenceEnumerable( +#endif + IEnumerable expected, + IEnumerable actual, + bool strict, + string prefix, + HashSet expectedRefs, + HashSet actualRefs, + int depth, + IReadOnlyList<(string Prefix, string Member)> exclusions) + { +#if XUNIT_NULLABLE + var expectedValues = expected.Cast().ToList(); + var actualValues = actual.Cast().ToList(); +#else + var expectedValues = expected.Cast().ToList(); + var actualValues = actual.Cast().ToList(); +#endif + var actualOriginalValues = actualValues.ToList(); + var collectionPrefix = prefix.Length == 0 ? string.Empty : prefix + "[]"; + + // Walk the list of expected values, and look for actual values that are equivalent + foreach (var expectedValue in expectedValues) + { + var actualIdx = 0; + + for (; actualIdx < actualValues.Count; ++actualIdx) + if (VerifyEquivalence(expectedValue, actualValues[actualIdx], strict, collectionPrefix, expectedRefs, actualRefs, depth, exclusions) == null) + break; + + if (actualIdx == actualValues.Count) + return EquivalentException.ForMissingCollectionValue(expectedValue, actualOriginalValues, collectionPrefix); + + actualValues.RemoveAt(actualIdx); + } + + if (strict && actualValues.Count != 0) + return EquivalentException.ForExtraCollectionValue(expectedValues, actualOriginalValues, actualValues, collectionPrefix); + + return null; + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceFileSystemInfo( +#else + static EquivalentException VerifyEquivalenceFileSystemInfo( +#endif + object expected, + object actual, + bool strict, + string prefix, + HashSet expectedRefs, + HashSet actualRefs, + int depth, + IReadOnlyList<(string Prefix, string Member)> exclusions) + { + if (fileSystemInfoFullNameProperty.Value == null) + throw new InvalidOperationException("Could not find 'FullName' property on type 'System.IO.FileSystemInfo'"); + + var expectedType = expected.GetType(); + var actualType = actual.GetType(); + + if (expectedType != actualType) + return EquivalentException.ForMismatchedTypes(expectedType, actualType, prefix); + + var fullName = fileSystemInfoFullNameProperty.Value.GetValue(expected); + var expectedAnonymous = new { FullName = fullName }; + + return VerifyEquivalenceReference(expectedAnonymous, actual, strict, prefix, expectedRefs, actualRefs, depth, exclusions); + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceGroupings( +#else + static EquivalentException VerifyEquivalenceGroupings( +#endif + object expected, + Type[] expectedGroupingTypes, + object actual, + Type[] actualGroupingTypes, + bool strict) + { + var expectedKey = typeof(IGrouping<,>).MakeGenericType(expectedGroupingTypes).GetRuntimeProperty("Key")?.GetValue(expected); + var actualKey = typeof(IGrouping<,>).MakeGenericType(actualGroupingTypes).GetRuntimeProperty("Key")?.GetValue(actual); + + var keyException = VerifyEquivalence(expectedKey, actualKey, strict: false); + if (keyException != null) + return keyException; + + var toArrayMethod = + typeof(Enumerable) + .GetRuntimeMethods() + .FirstOrDefault(m => m.IsStatic && m.IsPublic && m.Name == nameof(Enumerable.ToArray) && m.GetParameters().Length == 1) + ?? throw new InvalidOperationException("Could not find method Enumerable.ToArray<>"); + + // Convert everything to an array so it doesn't endlessly loop on the IGrouping<> test + var expectedToArrayMethod = toArrayMethod.MakeGenericMethod(expectedGroupingTypes[1]); + var expectedValues = expectedToArrayMethod.Invoke(null, new[] { expected }); + + var actualToArrayMethod = toArrayMethod.MakeGenericMethod(actualGroupingTypes[1]); + var actualValues = actualToArrayMethod.Invoke(null, new[] { actual }); + + if (VerifyEquivalence(expectedValues, actualValues, strict) != null) + throw EquivalentException.ForGroupingWithMismatchedValues(expectedValues, actualValues, ArgumentFormatter.Format(expectedKey)); + + return null; + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceIntrinsics( +#else + static EquivalentException VerifyEquivalenceIntrinsics( +#endif + object expected, + object actual, + string prefix) + { + var result = expected.Equals(actual); + + if (!result && TryConvert(expected, actual.GetType(), out var converted)) + result = converted.Equals(actual); + if (!result && TryConvert(actual, expected.GetType(), out converted)) + result = converted.Equals(expected); + + return result ? null : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceReference( +#else + static EquivalentException VerifyEquivalenceReference( +#endif + object expected, + object actual, + bool strict, + string prefix, + HashSet expectedRefs, + HashSet actualRefs, + int depth, + IReadOnlyList<(string Prefix, string Member)> exclusions) + { + Assert.GuardArgumentNotNull(nameof(prefix), prefix); + + var prefixDot = prefix.Length == 0 ? string.Empty : prefix + "."; + + // Enumerate over public instance fields and properties and validate equivalence + var expectedGetters = GetGettersForType(expected.GetType()); + var actualGetters = GetGettersForType(actual.GetType()); + + if (strict && expectedGetters.Count != actualGetters.Count) + return EquivalentException.ForMemberListMismatch(expectedGetters.Keys, actualGetters.Keys, prefixDot); + + var excludedAtThisLevel = + new HashSet( + exclusions + .Where(e => e.Prefix == prefix) + .Select(e => e.Member) + ); + + foreach (var kvp in expectedGetters) + { + if (excludedAtThisLevel.Contains(kvp.Key)) + continue; + + if (!actualGetters.TryGetValue(kvp.Key, out var actualGetter)) + return EquivalentException.ForMemberListMismatch(expectedGetters.Keys, actualGetters.Keys, prefixDot); + + var expectedMemberValue = kvp.Value(expected); + var actualMemberValue = actualGetter(actual); + + var ex = VerifyEquivalence(expectedMemberValue, actualMemberValue, strict, prefixDot + kvp.Key, expectedRefs, actualRefs, depth + 1, exclusions); + if (ex != null) + return ex; + } + + return null; + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceUri( +#else + static EquivalentException VerifyEquivalenceUri( +#endif + Uri expected, + Uri actual, + string prefix) + { + if (expected.OriginalString != actual.OriginalString) + return EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + + return null; + } + } + + sealed class ReferenceEqualityComparer : IEqualityComparer + { + public new bool Equals( +#if XUNIT_NULLABLE + object? x, + object? y) => +#else + object x, + object y) => +#endif + ReferenceEquals(x, y); + +#if XUNIT_NULLABLE + public int GetHashCode([DisallowNull] object obj) => +#else + public int GetHashCode(object obj) => +#endif + obj.GetHashCode(); + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertRangeComparer.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertRangeComparer.cs new file mode 100644 index 00000000000..b2160ea5608 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/AssertRangeComparer.cs @@ -0,0 +1,54 @@ +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8767 +#endif + +using System; +using System.Collections.Generic; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Sdk +{ + /// + /// Default implementation of used by the xUnit.net range assertions. + /// + /// The type that is being compared. + sealed class AssertRangeComparer : IComparer + where T : IComparable + { + /// + public int Compare( +#if XUNIT_NULLABLE + [AllowNull] T x, + [AllowNull] T y) +#else + T x, + T y) +#endif + { + // Null? + if (x == null && y == null) + return 0; + if (x == null) + return -1; + if (y == null) + return 1; + + // Same type? + if (x.GetType() != y.GetType()) + return -1; + + // Implements IComparable? + if (x is IComparable comparable1) + return comparable1.CompareTo(y); + + // Implements IComparable + return x.CompareTo(y); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker.cs new file mode 100644 index 00000000000..7a6046dfec5 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker.cs @@ -0,0 +1,946 @@ +#pragma warning disable CA1000 // Do not declare static members on generic types +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable CA1508 // Avoid dead conditional code +#pragma warning disable CA2213 // We move disposal to DisposeInternal, due to https://github.com/xunit/xunit/issues/2762 +#pragma warning disable IDE0019 // Use pattern matching +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0063 // Use simple 'using' statement +#pragma warning disable IDE0074 // Use compound assignment +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0290 // Use primary constructor +#pragma warning disable IDE0300 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8601 +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#pragma warning disable CS8618 +#pragma warning disable CS8625 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Sdk +{ + /// + /// Base class for generic , which also includes some public + /// static functionality. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + abstract partial class CollectionTracker : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + protected CollectionTracker(IEnumerable innerEnumerable) => + InnerEnumerable = innerEnumerable ?? throw new ArgumentNullException(nameof(innerEnumerable)); + + /// + /// Gets the inner enumerable that this collection track is wrapping. This is mostly + /// provided for simplifying other APIs which require both the tracker and the collection + /// (for example, ). + /// + protected internal IEnumerable InnerEnumerable { get; protected set; } + + /// + /// Determine if two enumerable collections are equal. It contains logic that varies depending + /// on the collection type (supporting arrays, dictionaries, sets, and generic enumerables). + /// + /// First value to compare + /// Second value to comare + /// The comparer used for individual item comparisons + /// Pass if the is the default item + /// comparer from ; pass , otherwise. + /// Returns if the collections are equal; , otherwise. + public static AssertEqualityResult AreCollectionsEqual( +#if XUNIT_NULLABLE + CollectionTracker? x, + CollectionTracker? y, +#else + CollectionTracker x, + CollectionTracker y, +#endif + IEqualityComparer itemComparer, + bool isDefaultItemComparer) + { + Assert.GuardArgumentNotNull(nameof(itemComparer), itemComparer); + + try + { + return + CheckIfDictionariesAreEqual(x, y) ?? + CheckIfSetsAreEqual(x, y, isDefaultItemComparer ? null : itemComparer) ?? + CheckIfArraysAreEqual(x, y, itemComparer, isDefaultItemComparer) ?? + CheckIfEnumerablesAreEqual(x, y, itemComparer, isDefaultItemComparer); + } + catch (Exception ex) + { + return AssertEqualityResult.ForResult(false, x?.InnerEnumerable, y?.InnerEnumerable, ex); + } + } + +#if XUNIT_NULLABLE + static AssertEqualityResult? CheckIfArraysAreEqual( + CollectionTracker? x, + CollectionTracker? y, +#else + static AssertEqualityResult CheckIfArraysAreEqual( + CollectionTracker x, + CollectionTracker y, +#endif + IEqualityComparer itemComparer, + bool isDefaultItemComparer) + { + if (x == null || y == null) + return null; + + var expectedArray = x.InnerEnumerable as Array; + var actualArray = y.InnerEnumerable as Array; + + if (expectedArray == null || actualArray == null) + return null; + + // If we have single-dimensional zero-based arrays, then we delegate to the enumerable + // version, since that's uses the trackers and gets us the mismatch pointer. + if (expectedArray.Rank == 1 && expectedArray.GetLowerBound(0) == 0 && + actualArray.Rank == 1 && actualArray.GetLowerBound(0) == 0) + return CheckIfEnumerablesAreEqual(x, y, itemComparer, isDefaultItemComparer); + + if (expectedArray.Rank != actualArray.Rank) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + + // Differing bounds, aka object[2,1] vs. object[1,2] + // You can also have non-zero-based arrays, so we don't just check lengths + for (var rank = 0; rank < expectedArray.Rank; rank++) + if (expectedArray.GetLowerBound(rank) != actualArray.GetLowerBound(rank) || expectedArray.GetUpperBound(rank) != actualArray.GetUpperBound(rank)) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + + // Enumeration will flatten everything identically, so just enumerate at this point + var expectedEnumerator = x.GetSafeEnumerator(); + var actualEnumerator = y.GetSafeEnumerator(); + + while (true) + { + var hasExpected = expectedEnumerator.MoveNext(); + var hasActual = actualEnumerator.MoveNext(); + + if (!hasExpected || !hasActual) + return AssertEqualityResult.ForResult(hasExpected == hasActual, x.InnerEnumerable, y.InnerEnumerable); + + if (!itemComparer.Equals(expectedEnumerator.Current, actualEnumerator.Current)) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + } + } + +#if XUNIT_NULLABLE + static AssertEqualityResult? CheckIfDictionariesAreEqual( + CollectionTracker? x, + CollectionTracker? y) +#else + static AssertEqualityResult CheckIfDictionariesAreEqual( + CollectionTracker x, + CollectionTracker y) +#endif + { + if (x == null || y == null) + return null; + + var dictionaryX = x.InnerEnumerable as IDictionary; + var dictionaryY = y.InnerEnumerable as IDictionary; + + if (dictionaryX == null || dictionaryY == null) + return null; + + if (dictionaryX.Count != dictionaryY.Count) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + + var dictionaryYKeys = new HashSet(dictionaryY.Keys.Cast()); + + // We don't pass along the itemComparer from AreCollectionsEqual because we aren't directly + // comparing the KeyValuePair<> objects. Instead we rely on Contains() on the dictionary to + // match up keys, and then create type-appropriate comparers for the values. + foreach (var key in dictionaryX.Keys.Cast()) + { + if (!dictionaryYKeys.Contains(key)) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + + var valueX = dictionaryX[key]; + var valueY = dictionaryY[key]; + + if (valueX == null) + { + if (valueY != null) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + } + else if (valueY == null) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + else + { + var valueXType = valueX.GetType(); + var valueYType = valueY.GetType(); + + var comparer = AssertEqualityComparer.GetDefaultComparer(valueXType == valueYType ? valueXType : typeof(object)); + if (!comparer.Equals(valueX, valueY)) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + } + + dictionaryYKeys.Remove(key); + } + + return AssertEqualityResult.ForResult(dictionaryYKeys.Count == 0, x.InnerEnumerable, y.InnerEnumerable); + } + + static AssertEqualityResult CheckIfEnumerablesAreEqual( +#if XUNIT_NULLABLE + CollectionTracker? x, + CollectionTracker? y, +#else + CollectionTracker x, + CollectionTracker y, +#endif + IEqualityComparer itemComparer, + bool isDefaultItemComparer) + { + if (x == null) + return AssertEqualityResult.ForResult(y == null, null, y?.InnerEnumerable); + if (y == null) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, null); + + var (comparisonType, equalsMethod) = GetAssertEqualityComparerMetadata(itemComparer); + var enumeratorX = x.GetSafeEnumerator(); + var enumeratorY = y.GetSafeEnumerator(); + var mismatchIndex = 0; + + while (true) + { + var hasNextX = enumeratorX.MoveNext(); + var hasNextY = enumeratorY.MoveNext(); + + if (!hasNextX || !hasNextY) + return hasNextX == hasNextY + ? AssertEqualityResult.ForResult(true, x.InnerEnumerable, y.InnerEnumerable) + : AssertEqualityResult.ForMismatch(x.InnerEnumerable, y.InnerEnumerable, mismatchIndex); + + var xCurrent = enumeratorX.Current; + var yCurrent = enumeratorY.Current; + + using (var xCurrentTracker = isDefaultItemComparer ? xCurrent.AsNonStringTracker() : null) + using (var yCurrentTracker = isDefaultItemComparer ? yCurrent.AsNonStringTracker() : null) + { + try + { + if (xCurrentTracker != null && yCurrentTracker != null) + { + var innerCompare = AreCollectionsEqual(xCurrentTracker, yCurrentTracker, AssertEqualityComparer.DefaultInnerComparer, true); + if (!innerCompare.Equal) + return AssertEqualityResult.ForMismatch(x.InnerEnumerable, y.InnerEnumerable, mismatchIndex, innerResult: innerCompare); + } + else + { + var assertEqualityResult = default(AssertEqualityResult); + if (comparisonType?.IsAssignableFrom(xCurrent?.GetType()) == true && comparisonType?.IsAssignableFrom(yCurrent?.GetType()) == true) + assertEqualityResult = equalsMethod?.Invoke(itemComparer, new[] { xCurrent, null, yCurrent, null }) as AssertEqualityResult; + + if (assertEqualityResult != null) + { + if (!assertEqualityResult.Equal) + return AssertEqualityResult.ForMismatch(x.InnerEnumerable, y.InnerEnumerable, mismatchIndex, innerResult: assertEqualityResult); + } + else if (!itemComparer.Equals(xCurrent, yCurrent)) + return AssertEqualityResult.ForMismatch(x.InnerEnumerable, y.InnerEnumerable, mismatchIndex); + } + } + catch (Exception ex) + { + return AssertEqualityResult.ForMismatch(x.InnerEnumerable, y.InnerEnumerable, mismatchIndex, ex); + } + + mismatchIndex++; + } + } + } + + static bool CompareTypedSets( + ISet setX, + ISet setY, +#if XUNIT_NULLABLE + IEqualityComparer? itemComparer) +#else + IEqualityComparer itemComparer) +#endif + { + if (setX.Count != setY.Count) + return false; + + if (itemComparer != null) + { + setX = new HashSet(setX, itemComparer); + setY = new HashSet(setY, itemComparer); + } + + return setX.SetEquals(setY); + } + + /// + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + /// + /// Override to provide an implementation of . + /// + /// + protected abstract void Dispose(bool disposing); + + /// + /// Formats the collection when you have a mismatched index. The formatted result will be the section of the + /// collection surrounded by the mismatched item. + /// + /// The index of the mismatched item + /// How many spaces into the output value the pointed-to item begins at + /// The optional printing depth (1 indicates a top-level value) + /// The formatted collection + public abstract string FormatIndexedMismatch( + int? mismatchedIndex, + out int? pointerIndent, + int depth = 1); + + /// + /// Formats the collection when you have a mismatched index. The formatted result will be the section of the + /// collection from to . These indices are usually + /// obtained by calling . + /// + /// The start index of the collection to print + /// The end index of the collection to print + /// The mismatched item index + /// How many spaces into the output value the pointed-to item begins at + /// The optional printing depth (1 indicates a top-level value) + /// The formatted collection + public abstract string FormatIndexedMismatch( + int startIndex, + int endIndex, + int? mismatchedIndex, + out int? pointerIndent, + int depth = 1); + + /// + /// Formats the beginning part of the collection. + /// + /// The optional printing depth (1 indicates a top-level value) + /// The formatted collection + public abstract string FormatStart(int depth = 1); + + /// + /// Gets the extents to print when you find a mismatched index, in the form of + /// a and . If the mismatched + /// index is , the extents will start at index 0. + /// + /// The mismatched item index + /// The start index that should be used for printing + /// The end index that should be used for printing + public abstract void GetMismatchExtents( + int? mismatchedIndex, + out int startIndex, + out int endIndex); + + /// + /// Gets a safe version of that prevents double enumeration and does all + /// the necessary tracking required for collection formatting. Should should be the same value + /// returned by , except non-generic. + /// + protected internal abstract IEnumerator GetSafeEnumerator(); + + /// + /// Gets the full name of the type of the element at the given index, if known. + /// Since this uses the item cache produced by enumeration, it may return + /// when we haven't enumerated enough to see the given element, or if we enumerated + /// so much that the item has left the cache, or if the item at the given index + /// is . It will also return when the + /// is . + /// + /// The item index +#if XUNIT_NULLABLE + public abstract string? TypeAt(int? index); +#else + public abstract string TypeAt(int? index); +#endif + + /// + /// Wraps an untyped enumerable in an object-based . + /// + /// The untyped enumerable to wrap + public static CollectionTracker Wrap(IEnumerable enumerable) => + new CollectionTracker(enumerable, enumerable.Cast()); + } + + /// + /// A utility class that can be used to wrap enumerables to prevent double enumeration. + /// It offers the ability to safely print parts of the collection when failures are + /// encountered, as well as some static versions of the printing functionality. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + sealed class CollectionTracker : CollectionTracker, IEnumerable + { + readonly IEnumerable collection; +#if XUNIT_NULLABLE + BufferedEnumerator? enumerator; +#else + BufferedEnumerator enumerator; +#endif + + /// + /// INTERNAL CONSTRUCTOR. DO NOT CALL. + /// + internal CollectionTracker( + IEnumerable collection, + IEnumerable castCollection) : + base(collection) => + this.collection = castCollection ?? throw new ArgumentNullException(nameof(castCollection)); + + CollectionTracker(IEnumerable collection) : + base(collection) => + this.collection = collection; + + /// + /// Gets the number of iterations that have happened so far. + /// + public int IterationCount => + enumerator == null ? 0 : enumerator.CurrentIndex + 1; + + /// + protected override void Dispose(bool disposing) => + enumerator?.DisposeInternal(); + + /// + public override string FormatIndexedMismatch( + int? mismatchedIndex, + out int? pointerIndent, + int depth = 1) + { + if (depth > ArgumentFormatter.MaxEnumerableLength) + { + pointerIndent = 1; + return ArgumentFormatter.EllipsisInBrackets; + } + + GetMismatchExtents(mismatchedIndex, out var startIndex, out var endIndex); + + return FormatIndexedMismatch( +#if XUNIT_NULLABLE + enumerator!.CurrentItemsIndexer, +#else + enumerator.CurrentItemsIndexer, +#endif + enumerator.MoveNext, + startIndex, + endIndex, + mismatchedIndex, + out pointerIndent, + depth + ); + } + + /// + public override string FormatIndexedMismatch( + int startIndex, + int endIndex, + int? mismatchedIndex, + out int? pointerIndent, + int depth = 1) + { + if (enumerator == null) + throw new InvalidOperationException("Called FormatIndexedMismatch with indices without calling GetMismatchExtents first"); + + return FormatIndexedMismatch( + enumerator.CurrentItemsIndexer, + enumerator.MoveNext, + startIndex, + endIndex, + mismatchedIndex, + out pointerIndent, + depth + ); + } + + /// + /// Formats a span with a mismatched index. + /// + /// The span to be formatted + /// The mismatched index point + /// How many spaces into the output value the pointed-to item begins at + /// The optional printing depth (1 indicates a top-level value) + /// The formatted span + public static string FormatIndexedMismatch( + ReadOnlySpan span, + int? mismatchedIndex, + out int? pointerIndent, + int depth = 1) + { + if (depth > ArgumentFormatter.MaxEnumerableLength) + { + pointerIndent = 1; + return ArgumentFormatter.EllipsisInBrackets; + } + + int startIndex, endIndex; + + if (ArgumentFormatter.MaxEnumerableLength == int.MaxValue) + { + startIndex = 0; + endIndex = span.Length - 1; + } + else + { + startIndex = Math.Max(0, (mismatchedIndex ?? 0) - ArgumentFormatter.MaxEnumerableLength / 2); + endIndex = Math.Min(span.Length - 1, startIndex + ArgumentFormatter.MaxEnumerableLength - 1); + startIndex = Math.Max(0, endIndex - ArgumentFormatter.MaxEnumerableLength + 1); + } + + var moreItemsPastEndIndex = endIndex < span.Length - 1; + var items = new Dictionary(); + + for (var idx = startIndex; idx <= endIndex; ++idx) + items[idx] = span[idx]; + + return FormatIndexedMismatch( + idx => items[idx], + () => moreItemsPastEndIndex, + startIndex, + endIndex, + mismatchedIndex, + out pointerIndent, + depth + ); + } + + static string FormatIndexedMismatch( + Func indexer, + Func moreItemsPastEndIndex, + int startIndex, + int endIndex, + int? mismatchedIndex, + out int? pointerIndent, + int depth) + { + pointerIndent = null; + + var printedValues = new StringBuilder("["); + if (startIndex != 0) + printedValues.Append(ArgumentFormatter.Ellipsis + ", "); + + for (var idx = startIndex; idx <= endIndex; ++idx) + { + if (idx != startIndex) + printedValues.Append(", "); + + if (idx == mismatchedIndex) + pointerIndent = printedValues.Length; + + printedValues.Append(ArgumentFormatter.Format(indexer(idx), depth)); + } + + if (moreItemsPastEndIndex()) + printedValues.Append(", " + ArgumentFormatter.Ellipsis); + + printedValues.Append(']'); + return printedValues.ToString(); + } + + /// + public override string FormatStart(int depth = 1) + { + if (depth > ArgumentFormatter.MaxEnumerableLength) + return ArgumentFormatter.EllipsisInBrackets; + + if (enumerator == null) + enumerator = BufferedEnumerator.Create(collection.GetEnumerator()); + + // Ensure we have already seen enough data to format + while (enumerator.CurrentIndex <= ArgumentFormatter.MaxEnumerableLength) + if (!enumerator.MoveNext()) + break; + + return FormatStart(enumerator.StartItemsIndexer, enumerator.CurrentIndex, depth); + } + + /// + /// Formats the beginning part of a collection. + /// + /// The collection to be formatted + /// The optional printing depth (1 indicates a top-level value) + /// The formatted collection + public static string FormatStart( + IEnumerable collection, + int depth = 1) + { + Assert.GuardArgumentNotNull(nameof(collection), collection); + + if (depth > ArgumentFormatter.MaxEnumerableLength) + return ArgumentFormatter.EllipsisInBrackets; + + var startItems = new List(); + var currentIndex = -1; + var spanEnumerator = collection.GetEnumerator(); + + // Ensure we have already seen enough data to format + while (currentIndex <= ArgumentFormatter.MaxEnumerableLength) + { + if (!spanEnumerator.MoveNext()) + break; + + startItems.Add(spanEnumerator.Current); + ++currentIndex; + } + + return FormatStart(idx => startItems[idx], currentIndex, depth); + } + + /// + /// Formats the beginning part of a span. + /// + /// The span to be formatted + /// The optional printing depth (1 indicates a top-level value) + /// The formatted span + public static string FormatStart( + ReadOnlySpan span, + int depth = 1) + { + if (depth > ArgumentFormatter.MaxEnumerableLength) + return ArgumentFormatter.EllipsisInBrackets; + + var startItems = new List(); + var currentIndex = -1; + var spanEnumerator = span.GetEnumerator(); + + // Ensure we have already seen enough data to format + while (currentIndex <= ArgumentFormatter.MaxEnumerableLength) + { + if (!spanEnumerator.MoveNext()) + break; + + startItems.Add(spanEnumerator.Current); + ++currentIndex; + } + + return FormatStart(idx => startItems[idx], currentIndex, depth); + } + + static string FormatStart( + Func indexer, + int currentIndex, + int depth) + { + var printedValues = new StringBuilder("["); + var printLength = Math.Min(currentIndex + 1, ArgumentFormatter.MaxEnumerableLength); + + for (var idx = 0; idx < printLength; ++idx) + { + if (idx != 0) + printedValues.Append(", "); + + printedValues.Append(ArgumentFormatter.Format(indexer(idx), depth)); + } + + if (currentIndex >= ArgumentFormatter.MaxEnumerableLength) + printedValues.Append(", " + ArgumentFormatter.Ellipsis); + + printedValues.Append(']'); + return printedValues.ToString(); + } + + /// + public IEnumerator GetEnumerator() + { + if (enumerator != null) + throw new InvalidOperationException("Multiple enumeration is not supported"); + + enumerator = BufferedEnumerator.Create(collection.GetEnumerator()); + return enumerator; + } + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + /// + protected internal override IEnumerator GetSafeEnumerator() => + GetEnumerator(); + + /// + public override void GetMismatchExtents( + int? mismatchedIndex, + out int startIndex, + out int endIndex) + { + if (enumerator == null) + enumerator = BufferedEnumerator.Create(collection.GetEnumerator()); + + if (ArgumentFormatter.MaxEnumerableLength == int.MaxValue) + { + startIndex = 0; + endIndex = int.MaxValue; + } + else + { + startIndex = Math.Max(0, (mismatchedIndex ?? 0) - ArgumentFormatter.MaxEnumerableLength / 2); + endIndex = startIndex + ArgumentFormatter.MaxEnumerableLength - 1; + } + + // Make sure our window starts with startIndex and ends with endIndex, as appropriate + while (enumerator.CurrentIndex < endIndex) + if (!enumerator.MoveNext()) + break; + + endIndex = enumerator.CurrentIndex; + + if (ArgumentFormatter.MaxEnumerableLength != int.MaxValue) + startIndex = Math.Max(0, endIndex - ArgumentFormatter.MaxEnumerableLength + 1); + } + + /// +#if XUNIT_NULLABLE + public override string? TypeAt(int? index) +#else + public override string TypeAt(int? index) +#endif + { + if (enumerator == null || !index.HasValue) + return null; + + if (!enumerator.TryGetCurrentItemAt(index.Value, out var item)) + return null; + + return item?.GetType().FullName; + } + + /// + /// Wraps the given collection inside of a . + /// + /// The collection to be wrapped + public static CollectionTracker Wrap(IEnumerable collection) => + new CollectionTracker(collection); + + abstract class BufferedEnumerator : IEnumerator + { + protected BufferedEnumerator(IEnumerator innerEnumerator) => + InnerEnumerator = innerEnumerator; + + public T Current => + InnerEnumerator.Current; + +#if XUNIT_NULLABLE + object? IEnumerator.Current => +#else + object IEnumerator.Current => +#endif + Current; + + public int CurrentIndex { get; private set; } = -1; + + public abstract Func CurrentItemsIndexer { get; } + + protected IEnumerator InnerEnumerator { get; } + + public abstract Func StartItemsIndexer { get; } + + public static BufferedEnumerator Create(IEnumerator innerEnumerator) => + ArgumentFormatter.MaxEnumerableLength == int.MaxValue + ? (BufferedEnumerator)new ListBufferedEnumerator(innerEnumerator) + : new RingBufferedEnumerator(innerEnumerator); + + public void Dispose() { } + + public void DisposeInternal() => + InnerEnumerator.Dispose(); + + public virtual bool MoveNext() + { + if (!InnerEnumerator.MoveNext()) + return false; + + CurrentIndex++; + return true; + } + + public virtual void Reset() + { + InnerEnumerator.Reset(); + + CurrentIndex = -1; + } + + public abstract bool TryGetCurrentItemAt( + int index, +#if XUNIT_NULLABLE + [MaybeNullWhen(false)] out T item); +#else + out T item); +#endif + + // Used when ArgumentFormatter.MaxEnumerableLength is unlimited (int.MaxValue) + sealed class ListBufferedEnumerator : BufferedEnumerator + { + readonly List buffer = new List(); + + public ListBufferedEnumerator(IEnumerator innerEnumerator) : + base(innerEnumerator) + { } + + public override Func CurrentItemsIndexer => + idx => buffer[idx]; + + public override Func StartItemsIndexer => + idx => buffer[idx]; + + public override bool MoveNext() + { + if (!base.MoveNext()) + return false; + + buffer.Add(InnerEnumerator.Current); + return true; + } + + public override void Reset() + { + base.Reset(); + + buffer.Clear(); + } + + public override bool TryGetCurrentItemAt( + int index, +#if XUNIT_NULLABLE + [MaybeNullWhen(false)] out T item) +#else + out T item) +#endif + { + if (index < 0 || index > CurrentIndex) + { + item = default; + return false; + } + + item = buffer[index]; + return true; + } + } + + // Used when ArgumentFormatter.MaxEnumerableLength is not unlimited + sealed class RingBufferedEnumerator : BufferedEnumerator + { + int currentItemsLastInsertionIndex = -1; + readonly T[] currentItemsRingBuffer = new T[ArgumentFormatter.MaxEnumerableLength]; + readonly List startItems = new List(); + + public override Func CurrentItemsIndexer + { + get + { + var result = new Dictionary(); + + if (CurrentIndex > -1) + { + var itemIndex = Math.Max(0, CurrentIndex - ArgumentFormatter.MaxEnumerableLength + 1); + + var indexInRingBuffer = (currentItemsLastInsertionIndex - CurrentIndex + itemIndex) % ArgumentFormatter.MaxEnumerableLength; + if (indexInRingBuffer < 0) + indexInRingBuffer += ArgumentFormatter.MaxEnumerableLength; + + while (itemIndex <= CurrentIndex) + { + result[itemIndex] = currentItemsRingBuffer[indexInRingBuffer]; + + ++itemIndex; + indexInRingBuffer = (indexInRingBuffer + 1) % ArgumentFormatter.MaxEnumerableLength; + } + } + + return idx => result[idx]; + } + } + + public override Func StartItemsIndexer => + idx => startItems[idx]; + + public RingBufferedEnumerator(IEnumerator innerEnumerator) : + base(innerEnumerator) + { } + + public override bool MoveNext() + { + if (!base.MoveNext()) + return false; + + var current = InnerEnumerator.Current; + + // Keep (MAX_ENUMERABLE_LENGTH + 1) items here, so we can + // print the start of the collection when lengths differ + if (CurrentIndex <= ArgumentFormatter.MaxEnumerableLength) + startItems.Add(current); + + // Keep a ring buffer filled with the most recent MAX_ENUMERABLE_LENGTH items + // so we can print out the items when we've found a bad index + currentItemsLastInsertionIndex = (currentItemsLastInsertionIndex + 1) % ArgumentFormatter.MaxEnumerableLength; + currentItemsRingBuffer[currentItemsLastInsertionIndex] = current; + + return true; + } + + public override void Reset() + { + base.Reset(); + + currentItemsLastInsertionIndex = -1; + startItems.Clear(); + } + + public override bool TryGetCurrentItemAt( + int index, +#if XUNIT_NULLABLE + [MaybeNullWhen(false)] out T item) +#else + out T item) +#endif + { + if (index < 0 || index <= CurrentIndex - ArgumentFormatter.MaxEnumerableLength || index > CurrentIndex) + { + item = default; + return false; + } + + var indexInRingBuffer = (currentItemsLastInsertionIndex - CurrentIndex + index) % ArgumentFormatter.MaxEnumerableLength; + if (indexInRingBuffer < 0) + indexInRingBuffer += ArgumentFormatter.MaxEnumerableLength; + + item = currentItemsRingBuffer[indexInRingBuffer]; + return true; + } + } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions.cs new file mode 100644 index 00000000000..38d56efe2ff --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions.cs @@ -0,0 +1,65 @@ +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8604 +#endif + +using System.Collections; +using System.Collections.Generic; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Sdk +{ + /// + /// Extension methods related to . + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + static partial class CollectionTrackerExtensions + { +#if XUNIT_NULLABLE + internal static CollectionTracker? AsNonStringTracker(this object? value) +#else + internal static CollectionTracker AsNonStringTracker(this object value) +#endif + { + if (value == null || value is string) + return null; + + return AsTracker(value as IEnumerable); + } + + /// + /// Wraps the given enumerable in an instance of . + /// + /// The item type of the collection + /// The enumerable to be wrapped +#if XUNIT_NULLABLE + [return: NotNullIfNotNull(nameof(enumerable))] + public static CollectionTracker? AsTracker(this IEnumerable? enumerable) => +#else + public static CollectionTracker AsTracker(this IEnumerable enumerable) => +#endif + enumerable == null + ? null + : enumerable as CollectionTracker ?? CollectionTracker.Wrap(enumerable); + + /// + /// Enumerates the elements inside the collection tracker. + /// + public static IEnumerator GetEnumerator(this CollectionTracker tracker) + { + Assert.GuardArgumentNotNull(nameof(tracker), tracker); + + return tracker.GetSafeEnumerator(); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions_aot.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions_aot.cs new file mode 100644 index 00000000000..61a7e0d0998 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions_aot.cs @@ -0,0 +1,42 @@ +#if XUNIT_AOT + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#endif + +using System.Collections; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Sdk +{ + partial class CollectionTrackerExtensions + { + /// + /// Wraps the given enumerable in an instance of . + /// + /// The enumerable to be wrapped +#if XUNIT_NULLABLE + [return: NotNullIfNotNull(nameof(enumerable))] + public static CollectionTracker? AsTracker(this IEnumerable? enumerable) +#else + public static CollectionTracker AsTracker(this IEnumerable enumerable) +#endif + { + if (enumerable == null) + return null; + + if (enumerable is CollectionTracker result) + return result; + + return CollectionTracker.Wrap(enumerable); + } + } +} + +#endif // XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions_reflection.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions_reflection.cs new file mode 100644 index 00000000000..2102c4781e0 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTrackerExtensions_reflection.cs @@ -0,0 +1,78 @@ +#if !XUNIT_AOT + +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0300 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8601 +#pragma warning disable CS8603 +#endif + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Sdk +{ + partial class CollectionTrackerExtensions + { +#if XUNIT_NULLABLE + static readonly MethodInfo? asTrackerOpenGeneric = +#else + static readonly MethodInfo asTrackerOpenGeneric = +#endif + typeof(CollectionTrackerExtensions).GetRuntimeMethods().FirstOrDefault(m => m.Name == nameof(AsTracker) && m.IsGenericMethod); + + static readonly ConcurrentDictionary cacheOfAsTrackerByType = new ConcurrentDictionary(); + + /// + /// Wraps the given enumerable in an instance of . + /// + /// The enumerable to be wrapped +#if XUNIT_NULLABLE + [return: NotNullIfNotNull(nameof(enumerable))] + public static CollectionTracker? AsTracker(this IEnumerable? enumerable) +#else + public static CollectionTracker AsTracker(this IEnumerable enumerable) +#endif + { + if (enumerable == null) + return null; + + if (enumerable is CollectionTracker result) + return result; + +#if XUNIT_AOT + return CollectionTracker.Wrap(enumerable); +#else + // CollectionTracker.Wrap for the non-T enumerable uses the CastIterator, which has terrible + // performance during iteration. We do our best to try to get a T and dynamically invoke the + // generic version of AsTracker as we can. + var iEnumerableOfT = enumerable.GetType().GetInterfaces().FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + if (iEnumerableOfT == null) + return CollectionTracker.Wrap(enumerable); + + var enumerableType = iEnumerableOfT.GenericTypeArguments[0]; +#if XUNIT_NULLABLE + var method = cacheOfAsTrackerByType.GetOrAdd(enumerableType, t => asTrackerOpenGeneric!.MakeGenericMethod(enumerableType)); +#else + var method = cacheOfAsTrackerByType.GetOrAdd(enumerableType, t => asTrackerOpenGeneric.MakeGenericMethod(enumerableType)); +#endif + + return method.Invoke(null, new object[] { enumerable }) as CollectionTracker ?? CollectionTracker.Wrap(enumerable); +#endif // XUNIT_AOT + } + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker_aot.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker_aot.cs new file mode 100644 index 00000000000..ac6f20b8153 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker_aot.cs @@ -0,0 +1,66 @@ +#if XUNIT_AOT +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8619 +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Xunit.Sdk +{ + partial class CollectionTracker + { +#if XUNIT_NULLABLE + static AssertEqualityResult? CheckIfSetsAreEqual( + CollectionTracker? x, + CollectionTracker? y, + IEqualityComparer? itemComparer) +#else + static AssertEqualityResult CheckIfSetsAreEqual( + CollectionTracker x, + CollectionTracker y, + IEqualityComparer itemComparer) +#endif + { + if (x == null || y == null) + return null; + + var elementTypeX = ArgumentFormatter.GetSetElementType(x.InnerEnumerable); + var elementTypeY = ArgumentFormatter.GetSetElementType(y.InnerEnumerable); + + if (elementTypeX == null || elementTypeY == null) + return null; + + if (elementTypeX != elementTypeY) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + + if (itemComparer == null) + return null; + + return AssertEqualityResult.ForResult( + CompareTypedSets( + (ISet)x.InnerEnumerable, + (ISet)y.InnerEnumerable, + (IEqualityComparer)itemComparer + ), + x.InnerEnumerable, + y.InnerEnumerable + ); + } + +#if XUNIT_NULLABLE + static (Type?, MethodInfo?) GetAssertEqualityComparerMetadata(IEqualityComparer itemComparer) => +#else + static (Type, MethodInfo) GetAssertEqualityComparerMetadata(IEqualityComparer itemComparer) => +#endif + (null, null); + } +} + +#endif // XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker_reflection.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker_reflection.cs new file mode 100644 index 00000000000..3b9d3f81584 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/CollectionTracker_reflection.cs @@ -0,0 +1,79 @@ +#if !XUNIT_AOT + +#pragma warning disable IDE0300 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#pragma warning disable CS8605 +#pragma warning disable CS8619 +#endif + +using System; +using System.Collections; +using System.Linq; +using System.Reflection; + +namespace Xunit.Sdk +{ + partial class CollectionTracker + { + static readonly MethodInfo openGenericCompareTypedSetsMethod = + typeof(CollectionTracker) + .GetRuntimeMethods() + .Single(m => m.Name == nameof(CompareTypedSets)); + +#if XUNIT_NULLABLE + static AssertEqualityResult? CheckIfSetsAreEqual( + CollectionTracker? x, + CollectionTracker? y, + IEqualityComparer? itemComparer) +#else + static AssertEqualityResult CheckIfSetsAreEqual( + CollectionTracker x, + CollectionTracker y, + IEqualityComparer itemComparer) +#endif + { + if (x == null || y == null) + return null; + + var elementTypeX = ArgumentFormatter.GetSetElementType(x.InnerEnumerable); + var elementTypeY = ArgumentFormatter.GetSetElementType(y.InnerEnumerable); + + if (elementTypeX == null || elementTypeY == null) + return null; + + if (elementTypeX != elementTypeY) + return AssertEqualityResult.ForResult(false, x.InnerEnumerable, y.InnerEnumerable); + + var genericCompareMethod = openGenericCompareTypedSetsMethod.MakeGenericMethod(elementTypeX); +#if XUNIT_NULLABLE + return AssertEqualityResult.ForResult((bool)genericCompareMethod.Invoke(null, new object?[] { x.InnerEnumerable, y.InnerEnumerable, itemComparer })!, x.InnerEnumerable, y.InnerEnumerable); +#else + return AssertEqualityResult.ForResult((bool)genericCompareMethod.Invoke(null, new object[] { x.InnerEnumerable, y.InnerEnumerable, itemComparer }), x.InnerEnumerable, y.InnerEnumerable); +#endif + } + +#if XUNIT_NULLABLE + static (Type?, MethodInfo?) GetAssertEqualityComparerMetadata(IEqualityComparer itemComparer) +#else + static (Type, MethodInfo) GetAssertEqualityComparerMetadata(IEqualityComparer itemComparer) +#endif + { + var assertQualityComparererType = + itemComparer + .GetType() + .GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAssertEqualityComparer<>)); + var comparisonType = assertQualityComparererType?.GenericTypeArguments[0]; + var equalsMethod = assertQualityComparererType?.GetMethod("Equals"); + + return (comparisonType, equalsMethod); + } + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/DynamicSkipToken.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/DynamicSkipToken.cs new file mode 100644 index 00000000000..43d235b930b --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/DynamicSkipToken.cs @@ -0,0 +1,18 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + static class DynamicSkipToken + { + /// + /// The contract for exceptions which indicate that something should be skipped rather than + /// failed is that exception message should start with this, and that any text following this + /// will be treated as the skip reason (for example, + /// "$XunitDynamicSkip$This code can only run on Linux") will result in a skipped test with + /// the reason of "This code can only run on Linux". + /// + public const string Value = "$XunitDynamicSkip$"; + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/EnvironmentVariables.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/EnvironmentVariables.cs new file mode 100644 index 00000000000..6472ecb156e --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/EnvironmentVariables.cs @@ -0,0 +1,22 @@ +namespace Xunit.Internal +{ + internal static class EnvironmentVariables + { + public const string AssertEquivalentMaxDepth = "XUNIT_ASSERT_EQUIVALENT_MAX_DEPTH"; + public const string HidePassingOutputDiagnostics = "XUNIT_HIDE_PASSING_OUTPUT_DIAGNOSTICS"; + public const string PrintMaxEnumerableLength = "XUNIT_PRINT_MAX_ENUMERABLE_LENGTH"; + public const string PrintMaxObjectDepth = "XUNIT_PRINT_MAX_OBJECT_DEPTH"; + public const string PrintMaxObjectMemberCount = "XUNIT_PRINT_MAX_OBJECT_MEMBER_COUNT"; + public const string PrintMaxStringLength = "XUNIT_PRINT_MAX_STRING_LENGTH"; + public const string TestingPlatformDebug = "XUNIT_TESTINGPLATFORM_DEBUG"; + + internal static class Defaults + { + public const int AssertEquivalentMaxDepth = 50; + public const int PrintMaxEnumerableLength = 5; + public const int PrintMaxObjectDepth = 3; + public const int PrintMaxObjectMemberCount = 5; + public const int PrintMaxStringLength = 50; + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/AllException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/AllException.cs new file mode 100644 index 00000000000..a982ea3eb08 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/AllException.cs @@ -0,0 +1,98 @@ +#pragma warning disable CA1032 // Implement standard exception constructors + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.All fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class AllException : XunitException + { + AllException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when one or + /// more items failed during + /// or , + /// , + /// or . + /// + /// The total number of items in the collection + /// The list of failures (as index, value, and exception) + public static AllException ForFailures( + int totalItems, + IReadOnlyList> errors) + { + Assert.GuardArgumentNotNull(nameof(errors), errors); + + var maxItemIndexLength = errors.Max(x => x.Item1).ToString(CultureInfo.CurrentCulture).Length + 4; // "[#]: " + var indexSpaces = new string(' ', maxItemIndexLength); + var maxWrapIndent = maxItemIndexLength + 7; // "Item: " and "Error: " + var wrapSpaces = Environment.NewLine + new string(' ', maxWrapIndent); + + var message = + string.Format( + CultureInfo.CurrentCulture, + "Assert.All() Failure: {0} out of {1} items in the collection did not pass.{2}{3}", + errors.Count, + totalItems, + Environment.NewLine, + string.Join( + Environment.NewLine, + errors.Select(error => + string.Format( + CultureInfo.CurrentCulture, + "{0}Item: {1}{2}{3}Error: {4}", + string.Format(CultureInfo.CurrentCulture, "[{0}]:", error.Item1).PadRight(maxItemIndexLength), +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + error.Item2.Replace(Environment.NewLine, wrapSpaces, StringComparison.Ordinal), +#else + error.Item2.Replace(Environment.NewLine, wrapSpaces), +#endif + Environment.NewLine, + indexSpaces, +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + error.Item3.Message?.Replace(Environment.NewLine, wrapSpaces, StringComparison.Ordinal) +#else + error.Item3.Message?.Replace(Environment.NewLine, wrapSpaces) +#endif + ) + ) + ) + ); + + return new AllException(message); + } + + /// + /// Creates a new instance of the class to be thrown when + /// collection is not supposed to be empty + /// during + /// or . + /// + public static AllException ForEmptyCollection() => + new AllException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.All() Failure: The collection was empty.{0}At least one item was expected.", + Environment.NewLine + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/CollectionException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/CollectionException.cs new file mode 100644 index 00000000000..98d3d193999 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/CollectionException.cs @@ -0,0 +1,109 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0300 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Globalization; +using System.Linq; + +// An "ExceptionUtility" class exists in both xunit.assert (Xunit.Internal.ExceptionUtility) as well as xunit.execution.dotnet (Xunit.Sdk.ExceptionUtility) +// This causes an compile-time error in projects that reference both the xunit.assert.source and xunit.core packages. +// To resolve this issue, add an alias for the Xunit.Internal.ExceptionUtility to make sure, the xunit.assert core uses the right ExceptionUtility +using ExceptionUtilityInternal = Xunit.Internal.ExceptionUtility; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Collection fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class CollectionException : XunitException + { + static readonly char[] crlfSeparators = new[] { '\r', '\n' }; + + CollectionException(string message) : + base(message) + { } + + static string FormatInnerException(Exception innerException) + { + var text = innerException.Message ?? string.Empty; + var filteredStack = ExceptionUtilityInternal.TransformStackTrace(ExceptionUtilityInternal.FilterStackTrace(innerException.StackTrace), " "); + if (!string.IsNullOrWhiteSpace(filteredStack)) + { + if (text.Length != 0) + text += Environment.NewLine; + + text += "Stack Trace:" + Environment.NewLine + filteredStack; + } + + var lines = + text + .Split(crlfSeparators, StringSplitOptions.RemoveEmptyEntries) + .Select((value, idx) => idx > 0 ? " " + value : value); + + return string.Join(Environment.NewLine, lines); + } + + /// + /// Creates an instance of the class to be thrown + /// when an item comparison failed + /// + /// The exception that was thrown + /// The item index for the failed item + /// The number of spaces needed to indent the failure pointer + /// The formatted collection + public static CollectionException ForMismatchedItem( + Exception exception, + int indexFailurePoint, + int? failurePointerIndent, + string formattedCollection) + { + Assert.GuardArgumentNotNull(nameof(exception), exception); + + var message = "Assert.Collection() Failure: Item comparison failure"; + + if (failurePointerIndent.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Collection: {1}{2}Error: {3}", Environment.NewLine, formattedCollection, Environment.NewLine, FormatInnerException(exception)); + + return new CollectionException(message); + } + + /// + /// Creates an instance of the class to be thrown + /// when the item count in a collection does not match the expected count. + /// + /// The expected item count + /// The actual item count + /// The formatted collection + public static CollectionException ForMismatchedItemCount( + int expectedCount, + int actualCount, + string formattedCollection) => + new CollectionException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Collection() Failure: Mismatched item count{0}Collection: {1}{2}Expected count: {3}{4}Actual count: {5}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(formattedCollection), formattedCollection), + Environment.NewLine, + expectedCount, + Environment.NewLine, + actualCount + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ContainsException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ContainsException.cs new file mode 100644 index 00000000000..182a9bb2804 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ContainsException.cs @@ -0,0 +1,168 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable CA1720 // Identifier contains type name +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; +using Xunit.Internal; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Contains fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class ContainsException : XunitException + { + ContainsException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when the requested filter did not match any items in the collection. + /// + /// The collection + public static ContainsException ForCollectionFilterNotMatched(string collection) => + new ContainsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Filter not matched in collection{0}Collection: {1}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(collection), collection) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the requested item was not available in the collection. + /// + /// The expected item value + /// The collection + public static ContainsException ForCollectionItemNotFound( + string item, + string collection) => + new ContainsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Item not found in collection{0}Collection: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(collection), collection), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(item), item) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the requested key was not available in the dictionary. + /// + /// The expected key value + /// The dictionary keys + public static ContainsException ForKeyNotFound( + string expectedKey, + string keys) => + new ContainsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Key not found in dictionary{0}Keys: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(keys), keys), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedKey), expectedKey) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the requested item was not found in the set. + /// + /// The expected item + /// The set + public static ContainsException ForSetItemNotFound( + string item, + string set) => + new ContainsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Item not found in set{0}Set: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(set), set), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(item), item) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the requested sub-memory was not found in the memory. + /// + /// The expected sub-memory + /// The memory + public static ContainsException ForSubMemoryNotFound( + string expectedSubMemory, + string memory) => + new ContainsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Sub-memory not found{0}Memory: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(memory), memory), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedSubMemory), expectedSubMemory) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the requested sub-span was not found in the span. + /// + /// The expected sub-span + /// The span + public static ContainsException ForSubSpanNotFound( + string expectedSubSpan, + string span) => + new ContainsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Sub-span not found{0}Span: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(span), span), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedSubSpan), expectedSubSpan) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the requested sub-string was not found in the string. + /// + /// The expected sub-string + /// The string + public static ContainsException ForSubStringNotFound( + string expectedSubString, +#if XUNIT_NULLABLE + string? @string) => +#else + string @string) => +#endif + new ContainsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Sub-string not found{0}String: {1}{2}Not found: {3}", + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(@string), + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(Assert.GuardArgumentNotNull(nameof(expectedSubString), expectedSubString)) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DistinctException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DistinctException.cs new file mode 100644 index 00000000000..6e7b162260f --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DistinctException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Distinct fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class DistinctException : XunitException + { + DistinctException(string message) : + base(message) + { } + + /// + /// Creates an instance of the class that is thrown + /// when a duplicate item is found in a collection. + /// + /// The duplicate item + /// The collection + public static DistinctException ForDuplicateItem( + string item, + string collection) => + new DistinctException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Distinct() Failure: Duplicate item found{0}Collection: {1}{2}Item: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(collection), collection), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(item), item) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DoesNotContainException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DoesNotContainException.cs new file mode 100644 index 00000000000..458d5fd171d --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DoesNotContainException.cs @@ -0,0 +1,206 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable CA1720 // Identifier contains type name +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; +using Xunit.Internal; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.DoesNotContain fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class DoesNotContainException : XunitException + { + DoesNotContainException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when the requested filter matches an item in the collection. + /// + /// The item index for where the item was found + /// The number of spaces needed to indent the failure pointer + /// The collection + public static DoesNotContainException ForCollectionFilterMatched( + int indexFailurePoint, + int? failurePointerIndent, + string collection) + { + Assert.GuardArgumentNotNull(nameof(collection), collection); + + var message = "Assert.DoesNotContain() Failure: Filter matched in collection"; + + if (failurePointerIndent.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Collection: {1}", Environment.NewLine, collection); + + return new DoesNotContainException(message); + } + + /// + /// Creates a new instance of the class to be thrown + /// when the requested item was found in the collection. + /// + /// The item that was found in the collection + /// The item index for where the item was found + /// The number of spaces needed to indent the failure pointer + /// The collection + public static DoesNotContainException ForCollectionItemFound( + string item, + int indexFailurePoint, + int? failurePointerIndent, + string collection) + { + Assert.GuardArgumentNotNull(nameof(item), item); + Assert.GuardArgumentNotNull(nameof(collection), collection); + + var message = "Assert.DoesNotContain() Failure: Item found in collection"; + + if (failurePointerIndent.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Collection: {1}{2}Found: {3}", Environment.NewLine, collection, Environment.NewLine, item); + + return new DoesNotContainException(message); + } + + /// + /// Creates a new instance of the class to be thrown + /// when the requested key was found in the dictionary. + /// + /// The expected key value + /// The dictionary keys + public static DoesNotContainException ForKeyFound( + string expectedKey, + string keys) => + new DoesNotContainException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.DoesNotContain() Failure: Key found in dictionary{0}Keys: {1}{2}Found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(keys), keys), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedKey), expectedKey) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the requested item was found in the set. + /// + /// The item that was found in the collection + /// The set + public static DoesNotContainException ForSetItemFound( + string item, + string set) => + new DoesNotContainException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.DoesNotContain() Failure: Item found in set{0}Set: {1}{2}Found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(set), set), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(item), item) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the requested sub-memory was found in the memory. + /// + /// The expected sub-memory + /// The item index for where the item was found + /// The number of spaces needed to indent the failure pointer + /// The memory + public static DoesNotContainException ForSubMemoryFound( + string expectedSubMemory, + int indexFailurePoint, + int? failurePointerIndent, + string memory) + { + Assert.GuardArgumentNotNull(nameof(expectedSubMemory), expectedSubMemory); + Assert.GuardArgumentNotNull(nameof(memory), memory); + + var message = "Assert.DoesNotContain() Failure: Sub-memory found"; + + if (failurePointerIndent.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Memory: {1}{2}Found: {3}", Environment.NewLine, memory, Environment.NewLine, expectedSubMemory); + + return new DoesNotContainException(message); + } + + /// + /// Creates a new instance of the class to be thrown + /// when the requested sub-span was found in the span. + /// + /// The expected sub-span + /// The item index for where the item was found + /// The number of spaces needed to indent the failure pointer + /// The span + public static DoesNotContainException ForSubSpanFound( + string expectedSubSpan, + int indexFailurePoint, + int? failurePointerIndent, + string span) + { + Assert.GuardArgumentNotNull(nameof(expectedSubSpan), expectedSubSpan); + Assert.GuardArgumentNotNull(nameof(span), span); + + var message = "Assert.DoesNotContain() Failure: Sub-span found"; + + if (failurePointerIndent.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Span: {1}{2}Found: {3}", Environment.NewLine, span, Environment.NewLine, expectedSubSpan); + + return new DoesNotContainException(message); + } + + /// + /// Creates a new instance of the class to be thrown + /// when the requested sub-string was found in the string. + /// + /// The expected sub-string + /// The item index for where the item was found + /// The string + public static DoesNotContainException ForSubStringFound( + string expectedSubString, + int indexFailurePoint, + string @string) + { + Assert.GuardArgumentNotNull(nameof(expectedSubString), expectedSubString); + Assert.GuardArgumentNotNull(nameof(@string), @string); + + var encodedString = AssertHelper.ShortenAndEncodeString(@string, indexFailurePoint, out var failurePointerIndent); + + return new DoesNotContainException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.DoesNotContain() Failure: Sub-string found{0} {1}\u2193 (pos {2}){3}String: {4}{5}Found: {6}", + Environment.NewLine, + new string(' ', failurePointerIndent), + indexFailurePoint, + Environment.NewLine, + encodedString, + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(expectedSubString) + ) + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DoesNotMatchException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DoesNotMatchException.cs new file mode 100644 index 00000000000..a1dcd8d0faa --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/DoesNotMatchException.cs @@ -0,0 +1,56 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable CA1720 // Identifier contains type name +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.DoesNotMatch fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class DoesNotMatchException : XunitException + { + DoesNotMatchException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class, thrown when + /// a regular expression matches the input string. + /// + /// The expected regular expression pattern + /// The item index for where the item was found + /// The number of spaces needed to indent the failure pointer + /// The string matched again + /// + public static DoesNotMatchException ForMatch( + string expectedRegexPattern, + int indexFailurePoint, + int failurePointerIndent, + string @string) => + new DoesNotMatchException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.DoesNotMatch() Failure: Match found{0} {1}\u2193 (pos {2}){3}String: {4}{5}RegEx: {6}", + Environment.NewLine, + new string(' ', failurePointerIndent), + indexFailurePoint, + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(@string), @string), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EmptyException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EmptyException.cs new file mode 100644 index 00000000000..59b0daa25e3 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EmptyException.cs @@ -0,0 +1,58 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; +using Xunit.Internal; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Empty fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class EmptyException : XunitException + { + EmptyException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the to be thrown + /// when the collection is not empty. + /// + /// The non-empty collection + public static EmptyException ForNonEmptyCollection(string collection) => + new EmptyException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Empty() Failure: Collection was not empty{0}Collection: {1}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(collection), collection) + ) + ); + + /// + /// Creates a new instance of the to be thrown + /// when the string is not empty. + /// + /// The non-empty string value + public static EmptyException ForNonEmptyString(string value) => + new EmptyException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Empty() Failure: String was not empty{0}String: {1}", + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(Assert.GuardArgumentNotNull(nameof(value), value)) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EndsWithException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EndsWithException.cs new file mode 100644 index 00000000000..2908ca815ef --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EndsWithException.cs @@ -0,0 +1,54 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; +using Xunit.Internal; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.EndsWith fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class EndsWithException : XunitException + { + EndsWithException(string message) : + base(message) + { } + + /// + /// Creates an instance of the class to be thrown + /// when a string does not end with the given value. + /// + /// The expected ending + /// The actual value + /// + public static EndsWithException ForStringNotFound( +#if XUNIT_NULLABLE + string? expected, + string? actual) => +#else + string expected, + string actual) => +#endif + new EndsWithException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.EndsWith() Failure: String end does not match{0}String: {1}{2}Expected end: {3}", + Environment.NewLine, + AssertHelper.ShortenAndEncodeStringEnd(actual), + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(expected) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EqualException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EqualException.cs new file mode 100644 index 00000000000..6b1a3d7827e --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EqualException.cs @@ -0,0 +1,367 @@ +#pragma warning disable CA1032 // Implement standard exception constructors + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#pragma warning disable CS8625 +#endif + +using System; +using System.ComponentModel; +using System.Globalization; +using Xunit.Internal; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Equal fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class EqualException : XunitException + { + static readonly string newLineAndIndent = Environment.NewLine + new string(' ', 10); // Length of "Expected: " and "Actual: " + + EqualException( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) : +#else + Exception innerException = null) : +#endif + base(message, innerException) + { } + + /// + /// Creates a new instance of to be thrown when two collections + /// are not equal. + /// + /// The index at which the collections differ + /// The expected collection + /// The spacing into the expected collection where the difference occurs + /// The type of the expected collection items, when they differ in type + /// The actual collection + /// The spacing into the actual collection where the difference occurs + /// The type of the actual collection items, when they differ in type + /// The display name for the collection type (defaults to "Collections") + public static EqualException ForMismatchedCollections( + int? mismatchedIndex, + string expected, + int? expectedPointer, +#if XUNIT_NULLABLE + string? expectedType, +#else + string expectedType, +#endif + string actual, + int? actualPointer, +#if XUNIT_NULLABLE + string? actualType, + string? collectionDisplay = null) => +#else + string actualType, + string collectionDisplay = null) => +#endif + ForMismatchedCollectionsWithError(mismatchedIndex, expected, expectedPointer, expectedType, actual, actualPointer, actualType, null, collectionDisplay); + + /// + /// Creates a new instance of to be thrown when two collections + /// are not equal, and an error has occurred during comparison. + /// + /// The index at which the collections differ + /// The expected collection + /// The spacing into the expected collection where the difference occurs + /// The type of the expected collection items, when they differ in type + /// The actual collection + /// The spacing into the actual collection where the difference occurs + /// The type of the actual collection items, when they differ in type + /// The optional exception that was thrown during comparison + /// The display name for the collection type (defaults to "Collections") + public static EqualException ForMismatchedCollectionsWithError( + int? mismatchedIndex, + string expected, + int? expectedPointer, +#if XUNIT_NULLABLE + string? expectedType, +#else + string expectedType, +#endif + string actual, + int? actualPointer, +#if XUNIT_NULLABLE + string? actualType, + Exception? error, + string? collectionDisplay = null) +#else + string actualType, + Exception error, + string collectionDisplay = null) +#endif + { + Assert.GuardArgumentNotNull(nameof(actual), actual); + + error = ArgumentFormatter.UnwrapException(error); + if (error is AssertEqualityComparer.OperationalFailureException) + return new EqualException("Assert.Equal() Failure: " + error.Message); + + var message = + error == null + ? string.Format(CultureInfo.CurrentCulture, "Assert.Equal() Failure: {0} differ", collectionDisplay ?? "Collections") + : "Assert.Equal() Failure: Exception thrown during comparison"; + + var expectedTypeText = expectedType != null && actualType != null && expectedType != actualType ? string.Format(CultureInfo.CurrentCulture, ", type {0}", expectedType) : ""; + var actualTypeText = expectedType != null && actualType != null && expectedType != actualType ? string.Format(CultureInfo.CurrentCulture, ", type {0}", actualType) : ""; + + if (expectedPointer.HasValue && mismatchedIndex.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2}{3})", Environment.NewLine, new string(' ', expectedPointer.Value), mismatchedIndex, expectedTypeText); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Expected: {1}{2}Actual: {3}", Environment.NewLine, expected, Environment.NewLine, actual); + + if (actualPointer.HasValue && mismatchedIndex.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2191 (pos {2}{3})", Environment.NewLine, new string(' ', actualPointer.Value), mismatchedIndex, actualTypeText); + + return new EqualException(message, error); + } + + /// + /// Creates a new instance of to be thrown when two sets + /// are not equal. + /// + /// The expected collection + /// The type of the expected set, when they differ in type + /// The actual collection + /// The type of the actual set, when they differ in type + /// The display name for the collection type + public static EqualException ForMismatchedSets( + string expected, +#if XUNIT_NULLABLE + string? expectedType, +#else + string expectedType, +#endif + string actual, +#if XUNIT_NULLABLE + string? actualType, +#else + string actualType, +#endif + string collectionDisplay) + { + Assert.GuardArgumentNotNull(nameof(expected), expected); + Assert.GuardArgumentNotNull(nameof(actual), actual); + + var message = string.Format(CultureInfo.CurrentCulture, "Assert.Equal() Failure: {0} differ", collectionDisplay); + var expectedTypeText = ""; + var actualTypeText = ""; + if (expectedType != null && actualType != null && expectedType != actualType) + { + var length = Math.Max(expectedType.Length, actualType.Length) + 1; + + expectedTypeText = expectedType.PadRight(length); + actualTypeText = actualType.PadRight(length); + } + + message += string.Format( + CultureInfo.CurrentCulture, + "{0}Expected: {1}{2}{3}Actual: {4}{5}", + Environment.NewLine, + expectedTypeText, + expected, + Environment.NewLine, + actualTypeText, + actual + ); + + return new EqualException(message); + } + + /// + /// Creates a new instance of to be thrown when two string + /// values are not equal. + /// + /// The expected value + /// The actual value + /// The index point in the expected string where the values differ + /// The index point in the actual string where the values differ + public static EqualException ForMismatchedStrings( +#if XUNIT_NULLABLE + string? expected, + string? actual, +#else + string expected, + string actual, +#endif + int expectedIndex, + int actualIndex) => + ForMismatchedStringsWithHeader(expected, actual, expectedIndex, actualIndex, "Strings differ"); + + /// + /// Creates a new instance of to be thrown when two string + /// values are not equal. + /// + /// The expected value + /// The actual value + /// The index point in the expected string where the values differ + /// The index point in the actual string where the values differ + /// The header to display in the assertion heading + public static EqualException ForMismatchedStringsWithHeader( +#if XUNIT_NULLABLE + string? expected, + string? actual, +#else + string expected, + string actual, +#endif + int expectedIndex, + int actualIndex, + string header) + { + var message = "Assert.Equal() Failure: " + header; + var (expectedStart, expectedEnd) = AssertHelper.GetStartEndForString(expected, expectedIndex); + var (actualStart, actualEnd) = AssertHelper.GetStartEndForString(actual, actualIndex); + + if ((expectedStart != 0 || actualStart != 0) && expectedIndex != -1 && actualIndex != -1) + { + // Try to find the correct start point so the positions will come into alignment + var positionDifference = expectedIndex - actualIndex; + var startingPosition = Math.Max(expectedStart, actualStart); + expectedStart = startingPosition; + actualStart = startingPosition - positionDifference; + } + + var formattedExpected = AssertHelper.ShortenString(expected, expectedStart, expectedEnd, expectedIndex, out var expectedPointer); + var formattedActual = AssertHelper.ShortenString(actual, actualStart, actualEnd, actualIndex, out var actualPointer); + + if (expected != null && expectedIndex > -1 && expectedIndex < expected.Length) + message += string.Format(CultureInfo.CurrentCulture, "{0}{1}\u2193 (pos {2})", newLineAndIndent, new string(' ', expectedPointer), expectedIndex); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Expected: {1}{2}Actual: {3}", Environment.NewLine, formattedExpected, Environment.NewLine, formattedActual); + + if (actual != null && expectedIndex > -1 && actualIndex < actual.Length) + message += string.Format(CultureInfo.CurrentCulture, "{0}{1}\u2191 (pos {2})", newLineAndIndent, new string(' ', actualPointer), actualIndex); + + return new EqualException(message); + } + + /// + /// Please use . + /// + [Obsolete("Please use the overload that accepts pre-formatted values as strings")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static EqualException ForMismatchedValues( +#if XUNIT_NULLABLE + object? expected, + object? actual, + string? banner = null) => +#else + object expected, + object actual, + string banner = null) => +#endif + ForMismatchedValues(ArgumentFormatter.Format(expected), ArgumentFormatter.Format(actual), banner); + + /// + /// Creates a new instance of to be thrown when two values + /// are not equal. This may be simple values (like intrinsics) or complex values (like + /// classes or structs). + /// + /// The expected value + /// The actual value + /// The banner to show; if , then the standard + /// banner of "Values differ" will be used + public static EqualException ForMismatchedValues( + string expected, + string actual, +#if XUNIT_NULLABLE + string? banner = null) => +#else + string banner = null) => +#endif + ForMismatchedValuesWithError(expected, actual, null, banner); + + /// + /// Please use . + /// + [Obsolete("Please use the overload that accepts pre-formatted values as strings")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static EqualException ForMismatchedValuesWithError( +#if XUNIT_NULLABLE + object? expected, + object? actual, + Exception? error = null, + string? banner = null) => +#else + object expected, + object actual, + Exception error = null, + string banner = null) => +#endif + // Strings normally come through ForMismatchedStrings, so we want to make sure any + // string value that comes through here isn't re-formatted/truncated. This allows + // any assertion functions which pre-format string values to preserve those. + ForMismatchedValuesWithError( + expected as string ?? ArgumentFormatter.Format(expected), + actual as string ?? ArgumentFormatter.Format(actual), + error, + banner + ); + + /// + /// Creates a new instance of to be thrown when two values + /// are not equal. This may be simple values (like intrinsics) or complex values (like + /// classes or structs). Used when an error has occurred during comparison. + /// + /// The expected value + /// The actual value + /// The optional exception that was thrown during comparison + /// The banner to show; if , then the standard + /// banner of "Values differ" will be used. If is not , + /// then the banner used will always be "Exception thrown during comparison", regardless + /// of the value passed here. + public static EqualException ForMismatchedValuesWithError( + string expected, + string actual, +#if XUNIT_NULLABLE + Exception? error = null, + string? banner = null) +#else + Exception error = null, + string banner = null) +#endif + { + Assert.GuardArgumentNotNull(nameof(expected), expected); + Assert.GuardArgumentNotNull(nameof(actual), actual); + + var message = + error == null + ? string.Format(CultureInfo.CurrentCulture, "Assert.Equal() Failure: {0}", banner ?? "Values differ") + : "Assert.Equal() Failure: Exception thrown during comparison"; + + return new EqualException( + string.Format( + CultureInfo.CurrentCulture, + "{0}{1}Expected: {2}{3}Actual: {4}", + message, + Environment.NewLine, +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + expected.Replace(Environment.NewLine, newLineAndIndent, StringComparison.Ordinal), +#else + expected.Replace(Environment.NewLine, newLineAndIndent), +#endif + Environment.NewLine, +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + actual.Replace(Environment.NewLine, newLineAndIndent, StringComparison.Ordinal) +#else + actual.Replace(Environment.NewLine, newLineAndIndent) +#endif + ), + error + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EquivalentException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EquivalentException.cs new file mode 100644 index 00000000000..06ef429e535 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/EquivalentException.cs @@ -0,0 +1,260 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable CA1200 // Avoid using cref tags with a prefix +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8625 +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Equivalent fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class EquivalentException : XunitException + { + EquivalentException( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) : +#else + Exception innerException = null) : +#endif + base(message, innerException) + { } + + static string FormatMemberNameList( + IEnumerable memberNames, + string prefix) => + string.Format( + CultureInfo.CurrentCulture, + "[{0}]", + string.Join(", ", memberNames.Select(k => string.Format(CultureInfo.CurrentCulture, "\"{0}{1}\"", prefix, k))) + ); + + /// + /// Creates a new instance of which shows a message that indicates + /// a circular reference was discovered. + /// + /// The name of the member that caused the circular reference + public static EquivalentException ForCircularReference(string memberName) => + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Circular reference found in '{0}'", + Assert.GuardArgumentNotNull(nameof(memberName), memberName) + ) + ); + + /// + /// Creates a new instance of which shows a message that indicates + /// that the maximum comparison depth was exceeded. + /// + /// The depth reached + /// The member access which caused the failure + public static EquivalentException ForExceededDepth( + int depth, + string memberName) => + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Exceeded the maximum depth {0} with '{1}'; check for infinite recursion or circular references", + depth, + Assert.GuardArgumentNotNull(nameof(memberName), memberName) + ) + ); + + /// + /// Creates a new instance of which shows a message that indicates + /// that the fault comes from an individual value mismatch one of the members. + /// + /// The expected member value + /// The actual member value + /// The name of the key with mismatched values + public static EquivalentException ForGroupingWithMismatchedValues( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + string keyName) => + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Grouping key [{0}] has mismatched values{1}Expected: {2}{3}Actual: {4}", + keyName, + Environment.NewLine, + ArgumentFormatter.Format(expected), + Environment.NewLine, + ArgumentFormatter.Format(actual) + ) + ); + + /// + /// Creates a new instance of which shows a message that indicates + /// that the list of available members does not match. + /// + /// The expected member names + /// The actual member names + /// The prefix to be applied to the member names (may be an empty string for a + /// top-level object, or a name in "member." format used as a prefix to show the member name list) + public static EquivalentException ForMemberListMismatch( + IEnumerable expectedMemberNames, + IEnumerable actualMemberNames, + string prefix) => + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Mismatched member list{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + FormatMemberNameList(Assert.GuardArgumentNotNull(nameof(expectedMemberNames), expectedMemberNames), prefix), + Environment.NewLine, + FormatMemberNameList(Assert.GuardArgumentNotNull(nameof(actualMemberNames), actualMemberNames), prefix) + ) + ); + + /// + /// Creates a new instance of which shows a message that indicates + /// that the fault comes from an individual value mismatch one of the members. + /// + /// The expected member value + /// The actual member value + /// The name of the mismatched member (may be an empty string for a + /// top-level object) + /// The inner exception that was thrown during value comparison, + /// typically during a call to + public static EquivalentException ForMemberValueMismatch( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + string memberName, +#if XUNIT_NULLABLE + Exception? innerException = null) => +#else + Exception innerException = null) => +#endif + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure{0}{1}Expected: {2}{3}Actual: {4}", + Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : string.Format(CultureInfo.CurrentCulture, ": Mismatched value on member '{0}'", memberName), + Environment.NewLine, + ArgumentFormatter.Format(expected), + Environment.NewLine, + ArgumentFormatter.Format(actual) + ), + innerException + ); + + /// + /// Creates a new instance of which shows a message that indicates + /// a value was missing from the collection. + /// + /// The object that was expected to be found in collection. + /// The actual collection which was missing the object. + /// The name of the member that was being inspected (may be an empty + /// string for a top-level collection) + public static EquivalentException ForMissingCollectionValue( +#if XUNIT_NULLABLE + object? expected, + IEnumerable actual, +#else + object expected, + IEnumerable actual, +#endif + string memberName) => + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Collection value not found{0}{1}Expected: {2}{3}In: {4}", + Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : string.Format(CultureInfo.CurrentCulture, " in member '{0}'", memberName), + Environment.NewLine, + ArgumentFormatter.Format(expected), + Environment.NewLine, + ArgumentFormatter.Format(actual) + ) + ); + + /// + /// Creates a new instance of which shows a message that indicates + /// that contained one or more values that were not specified + /// in . + /// + /// The values expected to be found in the + /// collection. + /// The actual collection values. + /// The values from that did not have + /// matching values + /// The name of the member that was being inspected (may be an empty + /// string for a top-level collection) + public static EquivalentException ForExtraCollectionValue( +#if XUNIT_NULLABLE + IEnumerable expected, + IEnumerable actual, + IEnumerable actualLeftovers, +#else + IEnumerable expected, + IEnumerable actual, + IEnumerable actualLeftovers, +#endif + string memberName) => + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Extra values found{0}{1}Expected: {2}{3}Actual: {4} left over from {5}", + Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : string.Format(CultureInfo.CurrentCulture, " in member '{0}'", memberName), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actualLeftovers), actualLeftovers)), + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) + ); + + /// + /// Creates a new instance of which shows a message that indicates + /// that does not match . This is typically + /// only used in special case comparison where it would be known that general comparison would fail + /// for other reasons, like two objects derived from with + /// different concrete types. + /// + /// The expected type + /// The actual type + /// The name of the member that was being inspected (may be an empty + /// string for a top-level comparison) + public static EquivalentException ForMismatchedTypes( + Type expectedType, + Type actualType, + string memberName) => + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Types did not match{0}{1}Expected type: {2}{3}Actual type: {4}", + Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : string.Format(CultureInfo.CurrentCulture, " in member '{0}'", memberName), + Environment.NewLine, + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(expectedType), expectedType), fullTypeName: true), + Environment.NewLine, + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(actualType), actualType), fullTypeName: true) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ExceptionUtility.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ExceptionUtility.cs new file mode 100644 index 00000000000..6ceec89f347 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ExceptionUtility.cs @@ -0,0 +1,99 @@ +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0300 // Simplify collection initialization +#pragma warning disable IDE0305 // Simplify collection initialization +#pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Xunit.Internal +{ + // Adapted from ExceptionUtility (xunit.v3.common) and StackFrameTransformer (xunit.v3.runner.common) + internal static class ExceptionUtility + { + static readonly Regex transformRegex = new Regex(@"^\s*at (?.*) in (?.*):(line )?(?\d+)$"); + + static bool FilterStackFrame(string stackFrame) + { + Assert.GuardArgumentNotNull(nameof(stackFrame), stackFrame); + +#if DEBUG + return false; +#else + return stackFrame.StartsWith("at Xunit.", StringComparison.Ordinal); +#endif + } + +#if XUNIT_NULLABLE + public static string? FilterStackTrace(string? stack) +#else + public static string FilterStackTrace(string stack) +#endif + { + if (stack == null) + return null; + + var results = new List(); + + foreach (var line in stack.Split(new[] { Environment.NewLine }, StringSplitOptions.None)) + { + var trimmedLine = line.TrimStart(); + if (!FilterStackFrame(trimmedLine)) + results.Add(line); + } + + return string.Join(Environment.NewLine, results.ToArray()); + } + +#if XUNIT_NULLABLE + public static string? TransformStackFrame( + string? stackFrame, +#else + public static string TransformStackFrame( + string stackFrame, +#endif + string indent = "") + { + if (stackFrame == null) + return null; + + var match = transformRegex.Match(stackFrame); + if (match == Match.Empty) + return stackFrame; + + var file = match.Groups["file"].Value; + return string.Format(CultureInfo.InvariantCulture, "{0}{1}({2},0): at {3}", indent, file, match.Groups["line"].Value, match.Groups["method"].Value); + } + +#if XUNIT_NULLABLE + public static string? TransformStackTrace( + string? stack, +#else + public static string TransformStackTrace( + string stack, +#endif + string indent = "") + { + if (stack == null) + return null; + + return string.Join( + Environment.NewLine, + stack + .Split(new[] { Environment.NewLine }, StringSplitOptions.None) + .Select(frame => TransformStackFrame(frame, indent)) + .ToArray() + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/FailException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/FailException.cs new file mode 100644 index 00000000000..1b9d48d4a85 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/FailException.cs @@ -0,0 +1,36 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Fail is called. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class FailException : XunitException + { + FailException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// the user calls . + /// + /// The user's failure message. +#if XUNIT_NULLABLE + public static FailException ForFailure(string? message) => +#else + public static FailException ForFailure(string message) => +#endif + new FailException(message ?? "Assert.Fail() Failure"); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/FalseException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/FalseException.cs new file mode 100644 index 00000000000..4f6961b01cd --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/FalseException.cs @@ -0,0 +1,50 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.False fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class FalseException : XunitException + { + FalseException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// a non- value was provided. + /// + /// The message to be displayed, or for the default message + /// The actual value + public static FalseException ForNonFalseValue( +#if XUNIT_NULLABLE + string? message, +#else + string message, +#endif + bool? value) => + new FalseException( + message ?? string.Format( + CultureInfo.CurrentCulture, + "Assert.False() Failure{0}Expected: False{1}Actual: {2}", + Environment.NewLine, + Environment.NewLine, + value?.ToString() ?? "null" + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/InRangeException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/InRangeException.cs new file mode 100644 index 00000000000..483df0b1445 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/InRangeException.cs @@ -0,0 +1,50 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.InRange fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class InRangeException : XunitException + { + InRangeException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// the given value is not in the given range. + /// + /// The actual object value + /// The low value of the range + /// The high value of the range + public static InRangeException ForValueNotInRange( + object actual, + object low, + object high) => + new InRangeException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.InRange() Failure: Value not in range{0}Range: ({1} - {2}){3}Actual: {4}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(low), low)), + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(high), high)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsAssignableFromException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsAssignableFromException.cs new file mode 100644 index 00000000000..fb4842a7368 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsAssignableFromException.cs @@ -0,0 +1,55 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.IsAssignableFrom fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class IsAssignableFromException : XunitException + { + IsAssignableFromException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// the value is not compatible with the given type. + /// + /// The expected type + /// The actual object value + public static IsAssignableFromException ForIncompatibleType( + Type expected, +#if XUNIT_NULLABLE + object? actual) => +#else + object actual) => +#endif + new IsAssignableFromException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.IsAssignableFrom() Failure: Value is {0}{1}Expected: {2}{3}Actual: {4}", + actual == null ? "null" : "an incompatible type", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(actual?.GetType()) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsNotAssignableFromException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsNotAssignableFromException.cs new file mode 100644 index 00000000000..58402838cc8 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsNotAssignableFromException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.IsNotAssignableFrom fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class IsNotAssignableFromException : XunitException + { + IsNotAssignableFromException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// the value is compatible with the given type. + /// + /// The expected type + /// The actual object value + public static IsNotAssignableFromException ForCompatibleType( + Type expected, + object actual) => + new IsNotAssignableFromException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.IsNotAssignableFrom() Failure: Value is a compatible type{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsNotTypeException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsNotTypeException.cs new file mode 100644 index 00000000000..25443c06e93 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsNotTypeException.cs @@ -0,0 +1,67 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.IsNotType fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class IsNotTypeException : XunitException + { + IsNotTypeException(string message) : + base(message) + { } + + static IsNotTypeException Create( + Type expectedType, + Type actualType, + string compatiblityMessage) + { + Assert.GuardArgumentNotNull(nameof(expectedType), expectedType); + Assert.GuardArgumentNotNull(nameof(actualType), actualType); + + return new IsNotTypeException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.IsNotType() Failure: Value is {0}{1}Expected: {2}{3}Actual: {4}", + compatiblityMessage, + Environment.NewLine, + ArgumentFormatter.Format(expectedType), + Environment.NewLine, + ArgumentFormatter.Format(actualType) + ) + ); + } + + /// + /// Creates a new instance of the class to be thrown + /// when the object is a compatible type. + /// + /// The expected type + /// The actual type + public static IsNotTypeException ForCompatibleType( + Type expectedType, + Type actualType) => + Create(expectedType, actualType, "a compatible type"); + + /// + /// Creates a new instance of the class to be thrown + /// when the object is the exact type. + /// + /// The expected type + public static IsNotTypeException ForExactType(Type type) => + Create(type, type, "the exact type"); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsTypeException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsTypeException.cs new file mode 100644 index 00000000000..9b3f3e2ce0c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/IsTypeException.cs @@ -0,0 +1,77 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.IsType fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class IsTypeException : XunitException + { + IsTypeException(string message) : + base(message) + { } + + static IsTypeException Create( + string expectedTypeName, +#if XUNIT_NULLABLE + string? actualTypeName, +#else + string actualTypeName, +#endif + string compatibilityMessage) => + new IsTypeException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.IsType() Failure: Value is {0}{1}Expected: {2}{3}Actual: {4}", + actualTypeName == null ? "null" : compatibilityMessage, + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedTypeName), expectedTypeName), + Environment.NewLine, + actualTypeName ?? "null" + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when an object was not compatible with the given type + /// + /// The expected type name + /// The actual type name + public static IsTypeException ForIncompatibleType( + string expectedTypeName, +#if XUNIT_NULLABLE + string? actualTypeName) => +#else + string actualTypeName) => +#endif + Create(expectedTypeName, actualTypeName, "an incompatible type"); + + /// + /// Creates a new instance of the class to be thrown + /// when an object did not exactly match the given type + /// + /// The expected type name + /// The actual type name + public static IsTypeException ForMismatchedType( + string expectedTypeName, +#if XUNIT_NULLABLE + string? actualTypeName) => +#else + string actualTypeName) => +#endif + Create(expectedTypeName, actualTypeName, "not the exact type"); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/MatchesException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/MatchesException.cs new file mode 100644 index 00000000000..a4c0c959c54 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/MatchesException.cs @@ -0,0 +1,51 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Matches fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class MatchesException : XunitException + { + MatchesException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// the regular expression pattern isn't found within the value. + /// + /// The expected regular expression pattern + /// The actual value + public static MatchesException ForMatchNotFound( + string expectedRegexPattern, +#if XUNIT_NULLABLE + string? actual) => +#else + string actual) => +#endif + new MatchesException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Matches() Failure: Pattern not found in value{0}Regex: {1}{2}Value: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern)), + Environment.NewLine, + ArgumentFormatter.Format(actual) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/MultipleException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/MultipleException.cs new file mode 100644 index 00000000000..6a624ef2323 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/MultipleException.cs @@ -0,0 +1,65 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0305 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Multiple fails w/ multiple errors (when a single error + /// occurs, it is thrown directly). + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class MultipleException : XunitException + { + MultipleException( + string assertionName, + IEnumerable innerExceptions) : + base("Assert." + assertionName + "() Failure: Multiple failures were encountered") + { + Assert.GuardArgumentNotNull(nameof(innerExceptions), innerExceptions); + + InnerExceptions = innerExceptions.ToList(); + } + + /// + /// Gets the list of inner exceptions that were thrown. + /// + public IReadOnlyCollection InnerExceptions { get; } + + /// +#if XUNIT_NULLABLE + public override string? StackTrace => +#else + public override string StackTrace => +#endif + "Inner stack traces:"; + + /// + /// Creates a new instance of the class to be thrown + /// when caught 2 or more exceptions. + /// + /// The inner exceptions + public static MultipleException ForFailures(IReadOnlyCollection innerExceptions) => + new MultipleException("Multiple", innerExceptions); + + /// + /// Creates a new instance of the class to be thrown + /// when caught 2 or more exceptions. + /// + /// The inner exceptions + public static MultipleException ForFailuresAsync(IReadOnlyCollection innerExceptions) => + new MultipleException("MultipleAsync", innerExceptions); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotEmptyException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotEmptyException.cs new file mode 100644 index 00000000000..7c4cedfb808 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotEmptyException.cs @@ -0,0 +1,31 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.NotEmpty fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class NotEmptyException : XunitException + { + NotEmptyException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when a collection was unexpectedly empty. + /// + public static NotEmptyException ForNonEmptyCollection() => + new NotEmptyException("Assert.NotEmpty() Failure: Collection was empty"); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotEqualException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotEqualException.cs new file mode 100644 index 00000000000..145a18d29b1 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotEqualException.cs @@ -0,0 +1,219 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#pragma warning disable CS8625 +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.NotEqual fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class NotEqualException : XunitException + { + NotEqualException( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) : +#else + Exception innerException = null) : +#endif + base(message, innerException) + { } + + /// + /// Creates a new instance of to be thrown when two collections + /// are equal. + /// + /// The expected collection + /// The actual collection + /// The display name for the collection type (defaults to "Collections") + public static NotEqualException ForEqualCollections( + string expected, + string actual, +#if XUNIT_NULLABLE + string? collectionDisplay = null) => +#else + string collectionDisplay = null) => +#endif + ForEqualCollectionsWithError(null, expected, null, actual, null, null, collectionDisplay); + + /// + /// Creates a new instance of to be thrown when two collections + /// are equal, and an error has occurred during comparison. + /// + /// The index at which the collections error occurred (should be + /// when is ) + /// The expected collection + /// The spacing into the expected collection where the difference occurs + /// (should be when is null) + /// The actual collection + /// The spacing into the actual collection where the difference occurs + /// (should be when is null) + /// The optional exception that was thrown during comparison + /// The display name for the collection type (defaults to "Collections") + public static NotEqualException ForEqualCollectionsWithError( + int? mismatchedIndex, + string expected, + int? expectedPointer, + string actual, + int? actualPointer, +#if XUNIT_NULLABLE + Exception? error = null, + string? collectionDisplay = null) +#else + Exception error = null, + string collectionDisplay = null) +#endif + { + Assert.GuardArgumentNotNull(nameof(expected), expected); + Assert.GuardArgumentNotNull(nameof(actual), actual); + + error = ArgumentFormatter.UnwrapException(error); + + if (error is AssertEqualityComparer.OperationalFailureException) + return new NotEqualException("Assert.NotEqual() Failure: " + error.Message); + + var message = + error == null + ? string.Format(CultureInfo.CurrentCulture, "Assert.NotEqual() Failure: {0} are equal", collectionDisplay ?? "Collections") + : "Assert.NotEqual() Failure: Exception thrown during comparison"; + + if (expectedPointer.HasValue && mismatchedIndex.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', expectedPointer.Value), mismatchedIndex); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Expected: Not {1}{2}Actual: {3}", Environment.NewLine, expected, Environment.NewLine, actual); + + if (actualPointer.HasValue && mismatchedIndex.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2191 (pos {2})", Environment.NewLine, new string(' ', actualPointer.Value), mismatchedIndex); + + return new NotEqualException(message, error); + } + + /// + /// Creates a new instance of to be thrown when two sets + /// are equal. + /// + /// The expected collection + /// The type of the expected set, when they differ in type + /// The actual collection + /// The type of the actual set, when they differ in type + /// The display name for the collection type + public static NotEqualException ForEqualSets( + string expected, +#if XUNIT_NULLABLE + string? expectedType, +#else + string expectedType, +#endif + string actual, +#if XUNIT_NULLABLE + string? actualType, +#else + string actualType, +#endif + string collectionDisplay) + { + Assert.GuardArgumentNotNull(nameof(expected), expected); + Assert.GuardArgumentNotNull(nameof(actual), actual); + + var message = string.Format(CultureInfo.CurrentCulture, "Assert.NotEqual() Failure: {0} are equal", collectionDisplay); + var expectedTypeText = ""; + var actualTypeText = ""; + if (expectedType != null && actualType != null && expectedType != actualType) + { + var length = Math.Max(expectedType.Length, actualType.Length) + 1; + + expectedTypeText = expectedType.PadRight(length); + actualTypeText = actualType.PadRight(length); + } + + message += string.Format( + CultureInfo.CurrentCulture, + "{0}Expected: Not {1}{2}{3}Actual: {4}{5}", + Environment.NewLine, + expectedTypeText, + expected, + Environment.NewLine, + actualTypeText, + actual + ); + + return new NotEqualException(message); + } + + /// + /// Creates a new instance of to be thrown when two values + /// are equal. This may be simple values (like intrinsics) or complex values (like + /// classes or structs). + /// + /// The expected value + /// The actual value + /// The banner to show; if , then the standard + /// banner of "Values are equal" will be used + public static NotEqualException ForEqualValues( + string expected, + string actual, +#if XUNIT_NULLABLE + string? banner = null) => +#else + string banner = null) => +#endif + ForEqualValuesWithError(expected, actual, null, banner); + + /// + /// Creates a new instance of to be thrown when two values + /// are equal. This may be simple values (like intrinsics) or complex values (like + /// classes or structs). Used when an error has occurred during comparison. + /// + /// The expected value + /// The actual value + /// The optional exception that was thrown during comparison + /// The banner to show; if , then the standard + /// banner of "Values are equal" will be used. If is not , + /// then the banner used will always be "Exception thrown during comparison", regardless + /// of the value passed here. + public static NotEqualException ForEqualValuesWithError( + string expected, + string actual, +#if XUNIT_NULLABLE + Exception? error = null, + string? banner = null) +#else + Exception error = null, + string banner = null) +#endif + { + var message = + error == null + ? string.Format(CultureInfo.CurrentCulture, "Assert.NotEqual() Failure: {0}", banner ?? "Values are equal") + : "Assert.NotEqual() Failure: Exception thrown during comparison"; + + return new NotEqualException( + string.Format( + CultureInfo.CurrentCulture, + "{0}{1}Expected: Not {2}{3}Actual: {4}", + message, + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ), + error + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotInRangeException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotInRangeException.cs new file mode 100644 index 00000000000..58679b76ef1 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotInRangeException.cs @@ -0,0 +1,50 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.NotInRange fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class NotInRangeException : XunitException + { + NotInRangeException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// a value was unexpected with the range of two other values. + /// + /// The actual object value + /// The low value of the range + /// The high value of the range + public static NotInRangeException ForValueInRange( + object actual, + object low, + object high) => + new NotInRangeException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.NotInRange() Failure: Value in range{0}Range: ({1} - {2}){3}Actual: {4}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(low), low)), + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(high), high)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotNullException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotNullException.cs new file mode 100644 index 00000000000..f17caafee5c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotNullException.cs @@ -0,0 +1,62 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.NotNull fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class NotNullException : XunitException + { + NotNullException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be + /// throw when a pointer is . + /// + /// The inner type of the value + public static Exception ForNullPointer(Type type) => + new NotNullException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.NotNull() Failure: Value of type '{0}*' is null", + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(type), type)) + ) + ); + + /// + /// Creates a new instance of the class to be + /// throw when a nullable struct is . + /// + /// The inner type of the value + public static Exception ForNullStruct(Type type) => + new NotNullException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.NotNull() Failure: Value of type 'Nullable<{0}>' does not have a value", + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(type), type)) + ) + ); + + /// + /// Creates a new instance of the class to be + /// thrown when a reference value is . + /// + public static NotNullException ForNullValue() => + new NotNullException("Assert.NotNull() Failure: Value is null"); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotRaisesException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotRaisesException.cs new file mode 100644 index 00000000000..1116d24fb75 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotRaisesException.cs @@ -0,0 +1,50 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.NotRaisedAny fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class NotRaisesException : XunitException + { + NotRaisesException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// an unexpected event was raised. + /// + public static NotRaisesException ForUnexpectedEvent() => + new NotRaisesException("Assert.NotRaisedAny() Failure: An unexpected event was raised"); + + /// + /// Creates a new instance of the class to be thrown when + /// an unexpected event (with data) was raised. + /// + /// The type of the event args that was unexpected + public static NotRaisesException ForUnexpectedEvent(Type unexpected) => + new NotRaisesException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.NotRaisedAny() Failure: An unexpected event was raised{0}Unexpected: {1}{2}Actual: An event was raised", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(unexpected), unexpected)), + Environment.NewLine + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotSameException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotSameException.cs new file mode 100644 index 00000000000..632a5845c2d --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotSameException.cs @@ -0,0 +1,31 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.NotSame fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class NotSameException : XunitException + { + NotSameException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when two values are the same instance. + /// + public static NotSameException ForSameValues() => + new NotSameException("Assert.NotSame() Failure: Values are the same instance"); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotStrictEqualException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotStrictEqualException.cs new file mode 100644 index 00000000000..be318811397 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NotStrictEqualException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.NotStrictEqual fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class NotStrictEqualException : XunitException + { + NotStrictEqualException(string message) : + base(message) + { } + + /// + /// Creates a new instance of to be thrown when two values + /// are strictly equal. + /// + /// The expected value + /// The actual value + public static NotStrictEqualException ForEqualValues( + string expected, + string actual) => + new NotStrictEqualException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.NotStrictEqual() Failure: Values are equal{0}Expected: Not {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NullException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NullException.cs new file mode 100644 index 00000000000..a44f356ec5e --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/NullException.cs @@ -0,0 +1,81 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Null fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class NullException : XunitException + { + NullException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when the given pointer value was unexpectedly not null. + /// + /// The inner type of the value + public static NullException ForNonNullPointer(Type type) => + new NullException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Null() Failure: Value of type '{0}*' is not null", + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(type), type)) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the given nullable struct was unexpectedly not null. + /// + /// The inner type of the value + /// The actual non- value + public static Exception ForNonNullStruct( + Type type, + T? actual) + where T : struct => + new NullException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Null() Failure: Value of type 'Nullable<{0}>' has a value{1}Expected: null{2}Actual: {3}", + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(type), type)), + Environment.NewLine, + Environment.NewLine, + ArgumentFormatter.Format(actual) + ) + ); + + /// + /// Creates a new instance of the class to be thrown + /// when the given value was unexpectedly not null. + /// + /// The actual non- value + public static NullException ForNonNullValue(object actual) => + new NullException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Null() Failure: Value is not null{0}Expected: null{1}Actual: {2}", + Environment.NewLine, + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ProperSubsetException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ProperSubsetException.cs new file mode 100644 index 00000000000..492698a5d49 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ProperSubsetException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.ProperSubset fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class ProperSubsetException : XunitException + { + ProperSubsetException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when a set is not a proper subset of another set + /// + /// The expected value + /// The actual value + public static ProperSubsetException ForFailure( + string expected, + string actual) => + new ProperSubsetException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.ProperSubset() Failure: Value is not a proper subset{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ProperSupersetException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ProperSupersetException.cs new file mode 100644 index 00000000000..28ac605c8ee --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ProperSupersetException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.ProperSuperset fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class ProperSupersetException : XunitException + { + ProperSupersetException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when a set is not a proper superset of another set + /// + /// The expected value + /// The actual value + public static ProperSupersetException ForFailure( + string expected, + string actual) => + new ProperSupersetException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.ProperSuperset() Failure: Value is not a proper superset{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/PropertyChangedException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/PropertyChangedException.cs new file mode 100644 index 00000000000..343ca3ce7c4 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/PropertyChangedException.cs @@ -0,0 +1,40 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.PropertyChanged fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class PropertyChangedException : XunitException + { + PropertyChangedException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when a property was unexpectedly not set. + /// + /// The name of the property that was expected to be changed. + public static PropertyChangedException ForUnsetProperty(string propertyName) => + new PropertyChangedException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.PropertyChanged() failure: Property '{0}' was not set", + Assert.GuardArgumentNotNull(nameof(propertyName), propertyName) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/RaisesAnyException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/RaisesAnyException.cs new file mode 100644 index 00000000000..6fb18529add --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/RaisesAnyException.cs @@ -0,0 +1,43 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.RaisesAny fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class RaisesAnyException : XunitException + { + RaisesAnyException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// no event was raised. + /// + /// The type of the event args that was expected + public static RaisesAnyException ForNoEvent(Type expected) => + new RaisesAnyException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.RaisesAny() Failure: No event was raised{0}Expected: {1}{2}Actual: No event was raised", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/RaisesException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/RaisesException.cs new file mode 100644 index 00000000000..5943ca65881 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/RaisesException.cs @@ -0,0 +1,70 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Raises fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class RaisesException : XunitException + { + RaisesException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// the raised event wasn't the expected type. + /// + /// The type of the event args that was expected + /// The type of the event args that was actually raised + public static RaisesException ForIncorrectType( + Type expected, + Type actual) => + new RaisesException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Raises() Failure: Wrong event type was raised{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) + ); + + /// + /// Creates a new instance of the class to be thrown when + /// no event (without data) was raised. + /// + public static RaisesException ForNoEvent() => + new RaisesException("Assert.Raises() Failure: No event was raised"); + + /// + /// Creates a new instance of the class to be thrown when + /// no event (with data) was raised. + /// + /// The type of the event args that was expected + public static RaisesException ForNoEvent(Type expected) => + new RaisesException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Raises() Failure: No event was raised{0}Expected: {1}{2}Actual: No event was raised", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SameException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SameException.cs new file mode 100644 index 00000000000..ae3fd870b81 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SameException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Same fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class SameException : XunitException + { + SameException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when two values are not the same instance. + /// + /// The expected value + /// The actual value + public static SameException ForFailure( + string expected, + string actual) => + new SameException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Same() Failure: Values are not the same instance{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SingleException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SingleException.cs new file mode 100644 index 00000000000..7fbf4645fa7 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SingleException.cs @@ -0,0 +1,109 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Single fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class SingleException : XunitException + { + SingleException(string errorMessage) + : base(errorMessage) + { } + + /// + /// Creates an new instance of the class to be thrown when + /// the collection didn't contain any values (or didn't contain the expected value). + /// + /// The expected value (set to for no expected value) + /// The collection + public static SingleException Empty( +#if XUNIT_NULLABLE + string? expected, +#else + string expected, +#endif + string collection) + { + Assert.GuardArgumentNotNull(nameof(collection), collection); + + if (expected == null) + return new SingleException("Assert.Single() Failure: The collection was empty"); + + return new SingleException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Single() Failure: The collection did not contain any matching items{0}Expected: {1}{2}Collection: {3}", + Environment.NewLine, + expected, + Environment.NewLine, + collection + ) + ); + } + + /// + /// Creates an new instance of the class to be thrown when + /// the collection more than one value (or contained more than one of the expected value). + /// + /// The number of items, or the number of matching items + /// The expected value (set to for no expected value) + /// The collection + /// The list of indices where matches occurred + public static SingleException MoreThanOne( + int count, +#if XUNIT_NULLABLE + string? expected, +#else + string expected, +#endif + string collection, + ICollection matchIndices) + { + Assert.GuardArgumentNotNull(nameof(collection), collection); + Assert.GuardArgumentNotNull(nameof(matchIndices), matchIndices); + + var message = string.Format( + CultureInfo.CurrentCulture, + "Assert.Single() Failure: The collection contained {0} {1}items", + count, + expected == null ? "" : "matching " + ); + + if (expected == null) + message += string.Format( + CultureInfo.CurrentCulture, + "{0}Collection: {1}", + Environment.NewLine, + collection + ); + else + message += string.Format( + CultureInfo.CurrentCulture, + "{0}Expected: {1}{2}Collection: {3}{4}Match indices: {5}", + Environment.NewLine, + expected, + Environment.NewLine, + collection, + Environment.NewLine, + string.Join(", ", matchIndices) + ); + + return new SingleException(message); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SkipException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SkipException.cs new file mode 100644 index 00000000000..b011d3372ee --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SkipException.cs @@ -0,0 +1,41 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Skip is called. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class SkipException : XunitException + { + SkipException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when a user wants to dynamically skip a test. Note that this only works in + /// v3 and later of xUnit.net, as it requires runtime infrastructure changes. + /// + public static SkipException ForSkip(string message) => + new SkipException( + string.Format( + CultureInfo.CurrentCulture, + "{0}{1}", + DynamicSkipToken.Value, + Assert.GuardArgumentNotNull(nameof(message), message) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/StartsWithException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/StartsWithException.cs new file mode 100644 index 00000000000..59ce1ca4370 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/StartsWithException.cs @@ -0,0 +1,54 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; +using Xunit.Internal; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.StartsWith fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class StartsWithException : XunitException + { + StartsWithException(string message) : + base(message) + { } + + /// + /// Creates an instance of the class to be thrown + /// when a string does not start with the given value. + /// + /// The expected start + /// The actual value + /// + public static StartsWithException ForStringNotFound( +#if XUNIT_NULLABLE + string? expected, + string? actual) => +#else + string expected, + string actual) => +#endif + new StartsWithException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.StartsWith() Failure: String start does not match{0}String: {1}{2}Expected start: {3}", + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(actual), + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(expected) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/StrictEqualException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/StrictEqualException.cs new file mode 100644 index 00000000000..6a04c0925fa --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/StrictEqualException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.StrictEqual fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class StrictEqualException : XunitException + { + StrictEqualException(string message) : + base(message) + { } + + /// + /// Creates a new instance of to be thrown when two values + /// are not strictly equal. + /// + /// The expected value + /// The actual value + public static StrictEqualException ForEqualValues( + string expected, + string actual) => + new StrictEqualException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.StrictEqual() Failure: Values differ{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SubsetException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SubsetException.cs new file mode 100644 index 00000000000..afdc31dab50 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SubsetException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Subset fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class SubsetException : XunitException + { + SubsetException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when a set is not a subset of another set + /// + /// The expected value + /// The actual value + public static SubsetException ForFailure( + string expected, + string actual) => + new SubsetException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Subset() Failure: Value is not a subset{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SupersetException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SupersetException.cs new file mode 100644 index 00000000000..5fbd0504489 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/SupersetException.cs @@ -0,0 +1,47 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Superset fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class SupersetException : XunitException + { + SupersetException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown + /// when a set is not a superset of another set + /// + /// The expected value + /// The actual value + public static SupersetException ForFailure( + string expected, + string actual) => + new SupersetException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Superset() Failure: Value is not a superset{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ThrowsAnyException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ThrowsAnyException.cs new file mode 100644 index 00000000000..8d6f8156fb2 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ThrowsAnyException.cs @@ -0,0 +1,87 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8625 +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.ThrowsAny fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class ThrowsAnyException : XunitException + { + ThrowsAnyException( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) : +#else + Exception innerException = null) : +#endif + base(message, innerException) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// an exception of the wrong type was thrown by Assert.ThrowsAny. + /// + /// The expected exception type + /// The actual exception + public static ThrowsAnyException ForIncorrectExceptionType( + Type expected, + Exception actual) => + new ThrowsAnyException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.ThrowsAny() Failure: Exception type was not compatible{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()) + ), + actual + ); + + /// + /// Creates a new instance of the class to be thrown when + /// an inspector rejected the exception. + /// + /// The custom message + /// The optional exception thrown by the inspector + public static Exception ForInspectorFailure( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) => +#else + Exception innerException = null) => +#endif + new ThrowsAnyException(string.Format(CultureInfo.CurrentCulture, "Assert.ThrowsAny() Failure: {0}", message), innerException); + + /// + /// Creates a new instance of the class to be thrown when + /// an exception wasn't thrown by Assert.ThrowsAny. + /// + /// The expected exception type + public static ThrowsAnyException ForNoException(Type expected) => + new ThrowsAnyException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.ThrowsAny() Failure: No exception was thrown{0}Expected: {1}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ThrowsException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ThrowsException.cs new file mode 100644 index 00000000000..e3160a31c4b --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/ThrowsException.cs @@ -0,0 +1,116 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8625 +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Throws fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class ThrowsException : XunitException + { + ThrowsException( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) : +#else + Exception innerException = null) : +#endif + base(message, innerException) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// an exception of the wrong type was thrown by Assert.Throws. + /// + /// The expected exception type + /// The actual exception + public static ThrowsException ForIncorrectExceptionType( + Type expected, + Exception actual) => + new ThrowsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Throws() Failure: Exception type was not an exact match{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()) + ), + actual + ); + + /// + /// Creates a new instance of the class to be thrown when + /// an is thrown with the wrong parameter name. + /// + /// The exception type + /// The expected parameter name + /// The actual parameter name + public static ThrowsException ForIncorrectParameterName( + Type expected, +#if XUNIT_NULLABLE + string? expectedParamName, + string? actualParamName) => +#else + string expectedParamName, + string actualParamName) => +#endif + new ThrowsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Throws() Failure: Incorrect parameter name{0}Exception: {1}{2}Expected: {3}{4}Actual: {5}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(expectedParamName), + Environment.NewLine, + ArgumentFormatter.Format(actualParamName) + ) + ); + + /// + /// Creates a new instance of the class to be thrown when + /// an inspector rejected the exception. + /// + /// The custom message + /// The optional exception thrown by the inspector + public static Exception ForInspectorFailure( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) => +#else + Exception innerException = null) => +#endif + new ThrowsException(string.Format(CultureInfo.CurrentCulture, "Assert.Throws() Failure: {0}", message), innerException); + + /// + /// Creates a new instance of the class to be thrown when + /// an exception wasn't thrown by Assert.Throws. + /// + /// The expected exception type + public static ThrowsException ForNoException(Type expected) => + new ThrowsException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Throws() Failure: No exception was thrown{0}Expected: {1}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/TrueException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/TrueException.cs new file mode 100644 index 00000000000..25c6f95b49f --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/TrueException.cs @@ -0,0 +1,51 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.True fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class TrueException : XunitException + { + TrueException(string message) : + base(message) + { } + + /// + /// Creates a new instance of the class to be thrown when + /// a non- value was provided. + /// + /// The message to be displayed, or for the default message + /// The actual value + public static TrueException ForNonTrueValue( +#if XUNIT_NULLABLE + string? message, +#else + string message, +#endif + bool? value) => + new TrueException( + message ?? + string.Format( + CultureInfo.CurrentCulture, + "Assert.True() Failure{0}Expected: True{1}Actual: {2}", + Environment.NewLine, + Environment.NewLine, + value?.ToString() ?? "null" + ) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/XunitException.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/XunitException.cs new file mode 100644 index 00000000000..9b60f06b0ca --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/Exceptions/XunitException.cs @@ -0,0 +1,77 @@ +#pragma warning disable CA1032 // Implement standard exception constructors +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0290 // Use primary constructor + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8625 +#endif + +using System; +using System.Globalization; + +namespace Xunit.Sdk +{ + /// + /// The base assert exception class. It marks itself with which is how + /// the framework differentiates between assertion fails and general exceptions. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class XunitException : Exception +#if !XUNIT_AOT + , IAssertionException +#endif + { + /// + /// Initializes a new instance of the class. + /// + /// The user message to be displayed + public XunitException( +#if XUNIT_NULLABLE + string? userMessage) : +#else + string userMessage) : +#endif + this(userMessage, null) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The user message to be displayed + /// The inner exception + public XunitException( +#if XUNIT_NULLABLE + string? userMessage, + Exception? innerException) : +#else + string userMessage, + Exception innerException) : +#endif + base(userMessage, innerException) + { } + + /// + public override string ToString() + { + var className = GetType().ToString(); + var message = Message; + var result = + message is null || message.Length == 0 + ? className + : string.Format(CultureInfo.CurrentCulture, "{0}: {1}", className, message); + + var stackTrace = StackTrace; + if (stackTrace != null) + result = string.Format(CultureInfo.CurrentCulture, "{0}{1}{2}", result, Environment.NewLine, stackTrace); + + return result; + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertEqualityComparer.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertEqualityComparer.cs new file mode 100644 index 00000000000..ad178940846 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertEqualityComparer.cs @@ -0,0 +1,85 @@ +#pragma warning disable CA1510 // Use ArgumentNullException throw helper +#pragma warning disable IDE0063 // Use simple 'using' statement + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Collections.Generic; + +namespace Xunit.Sdk +{ + /// + /// Represents a specialized version of that returns information useful + /// when formatting results for assertion failures. + /// + /// The type of the objects being compared. +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + interface IAssertEqualityComparer : IEqualityComparer + { + /// + /// Compares two values and determines if they are equal. + /// + /// The first value + /// The first value as a (if it's a collection) + /// The second value + /// The second value as a (if it's a collection) + /// Success or failure information + AssertEqualityResult Equals( +#if XUNIT_NULLABLE + T? x, + CollectionTracker? xTracker, + T? y, + CollectionTracker? yTracker); +#else + T x, + CollectionTracker xTracker, + T y, + CollectionTracker yTracker); +#endif + } + + /// + /// Extension methods for + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + static class IAssertEqualityComparerExtensions + { + /// + /// Compares two values and determines if they are equal. + /// + /// The comparer + /// The first value + /// The second value + /// Success or failure information + public static AssertEqualityResult Equals( + this IAssertEqualityComparer comparer, +#if XUNIT_NULLABLE + T? x, + T? y) +#else + T x, + T y) +#endif + { + if (comparer is null) + throw new ArgumentNullException(nameof(comparer)); + + using (var xTracker = x.AsNonStringTracker()) + using (var yTracker = y.AsNonStringTracker()) + return comparer.Equals(x, xTracker, y, yTracker); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertionException_aot.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertionException_aot.cs new file mode 100644 index 00000000000..bff648802f5 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertionException_aot.cs @@ -0,0 +1,28 @@ +#if XUNIT_AOT + +#pragma warning disable CA1040 // Avoid empty interfaces +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// This interface is not supported in Native AOT, because interface-based discovery + /// requires reflection. + /// + [Obsolete("This interface is not supported in Native AOT. Decorating with it is benign and ignored.")] +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + interface IAssertionException + { } +} + +#endif // XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertionException_reflection.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertionException_reflection.cs new file mode 100644 index 00000000000..fc1c9022192 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/IAssertionException_reflection.cs @@ -0,0 +1,26 @@ +#if !XUNIT_AOT + +#pragma warning disable CA1040 // Avoid empty interfaces +#pragma warning disable CA1200 // Avoid using cref tags with a prefix +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix + +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// This is a marker interface implemented by all built-in assertion exceptions so that + /// test failures can be marked with . + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + interface IAssertionException + { } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/OverloadResolutionPriorityAttribute.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/OverloadResolutionPriorityAttribute.cs new file mode 100644 index 00000000000..7383536eec1 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/OverloadResolutionPriorityAttribute.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET9_0_OR_GREATER && XUNIT_OVERLOAD_RESOLUTION_PRIORITY + +#pragma warning disable IDE0290 // Use primary constructor + +namespace System.Runtime.CompilerServices +{ + /// + /// Specifies the priority of a member in overload resolution. When unspecified, the default priority is 0. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + internal sealed class OverloadResolutionPriorityAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The priority of the attributed member. Higher numbers are prioritized, lower numbers are deprioritized. 0 is the default if no attribute is present. + public OverloadResolutionPriorityAttribute(int priority) => + Priority = priority; + + /// + /// The priority of the member. + /// + public int Priority { get; } + } +} + +#endif diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/StringAssertEqualityComparer.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/StringAssertEqualityComparer.cs new file mode 100644 index 00000000000..2ec597c7d67 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/StringAssertEqualityComparer.cs @@ -0,0 +1,245 @@ +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0090 // Use 'new(...)' +#pragma warning disable IDE0290 // Use primary constructor + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Collections.Generic; + +namespace Xunit.Sdk +{ + /// + /// This static class offers equivalence comparisons for string values + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + static class StringAssertEqualityComparer + { + static readonly HashSet charsLineEndings = new HashSet() + { + '\r', // Carriage Return + '\n', // Line feed + }; + static readonly HashSet charsWhitespace = new HashSet() + { + '\t', // Tab + ' ', // Space + '\u00A0', // No-Break Space + '\u1680', // Ogham Space Mark + '\u180E', // Mongolian Vowel Separator + '\u2000', // En Quad + '\u2001', // Em Quad + '\u2002', // En Space + '\u2003', // Em Space + '\u2004', // Three-Per-Em Space + '\u2005', // Four-Per-Em Space + '\u2006', // Six-Per-Em Space + '\u2007', // Figure Space + '\u2008', // Punctuation Space + '\u2009', // Thin Space + '\u200A', // Hair Space + '\u200B', // Zero Width Space + '\u202F', // Narrow No-Break Space + '\u205F', // Medium Mathematical Space + '\u3000', // Ideographic Space + '\uFEFF', // Zero Width No-Break Space + }; + + /// + /// Compare two string values for equalivalence. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static AssertEqualityResult Equivalent( +#if XUNIT_NULLABLE + string? expected, + string? actual, +#else + string expected, + string actual, +#endif + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) + { + if (expected == null && actual == null) + return AssertEqualityResult.ForResult(true, expected, actual); + if (expected == null || actual == null) + return AssertEqualityResult.ForResult(false, expected, actual); + + return Equivalent(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + } + + /// + /// Compare two string values for equalivalence. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static AssertEqualityResult Equivalent( + ReadOnlySpan expected, + ReadOnlySpan actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) + { + // Walk the string, keeping separate indices since we can skip variable amounts of + // data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences. + var expectedIndex = 0; + var actualIndex = 0; + var expectedLength = expected.Length; + var actualLength = actual.Length; + + // Block used to fix edge case of Equal("", " ") when ignoreAllWhiteSpace enabled. + if (ignoreAllWhiteSpace) + { + if (expectedLength == 0 && SkipWhitespace(actual, 0) == actualLength) + return AssertEqualityResult.ForResult(true, expected.ToString(), actual.ToString()); + if (actualLength == 0 && SkipWhitespace(expected, 0) == expectedLength) + return AssertEqualityResult.ForResult(true, expected.ToString(), actual.ToString()); + } + + while (expectedIndex < expectedLength && actualIndex < actualLength) + { + var expectedChar = expected[expectedIndex]; + var actualChar = actual[actualIndex]; + + if (ignoreLineEndingDifferences && charsLineEndings.Contains(expectedChar) && charsLineEndings.Contains(actualChar)) + { + expectedIndex = SkipLineEnding(expected, expectedIndex); + actualIndex = SkipLineEnding(actual, actualIndex); + } + else if (ignoreAllWhiteSpace && (charsWhitespace.Contains(expectedChar) || charsWhitespace.Contains(actualChar))) + { + expectedIndex = SkipWhitespace(expected, expectedIndex); + actualIndex = SkipWhitespace(actual, actualIndex); + } + else if (ignoreWhiteSpaceDifferences && charsWhitespace.Contains(expectedChar) && charsWhitespace.Contains(actualChar)) + { + expectedIndex = SkipWhitespace(expected, expectedIndex); + actualIndex = SkipWhitespace(actual, actualIndex); + } + else + { + if (ignoreCase) + { + expectedChar = char.ToUpperInvariant(expectedChar); + actualChar = char.ToUpperInvariant(actualChar); + } + + if (expectedChar != actualChar) + break; + + expectedIndex++; + actualIndex++; + } + } + + if (expectedIndex < expectedLength || actualIndex < actualLength) + return AssertEqualityResult.ForMismatch(expected.ToString(), actual.ToString(), expectedIndex, actualIndex); + + return AssertEqualityResult.ForResult(true, expected.ToString(), actual.ToString()); + } + + static int SkipLineEnding( + ReadOnlySpan value, + int index) + { + if (value[index] == '\r') + ++index; + + if (index < value.Length && value[index] == '\n') + ++index; + + return index; + } + + static int SkipWhitespace( + ReadOnlySpan value, + int index) + { + while (index < value.Length) + { + if (charsWhitespace.Contains(value[index])) + index++; + else + return index; + } + + return index; + } + + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/Sdk/StringSyntaxAttribute.cs b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/StringSyntaxAttribute.cs new file mode 100644 index 00000000000..445241d7f95 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/Sdk/StringSyntaxAttribute.cs @@ -0,0 +1,126 @@ +#pragma warning disable IDE0301 // Simplify collection initialization + +#if XUNIT_NULLABLE +#nullable enable +#endif + +// Adapted from https://github.com/dotnet/runtime/blob/1e9c6a82aca4904828636b3638962c05a5f8d9c8/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs +// to polyfill Visual Studio syntax coloring support for pre-.NET 7 + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET7_0_OR_GREATER + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies the syntax used in a string. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + internal sealed class StringSyntaxAttribute : Attribute + { + /// + /// Initializes the with the identifier of the syntax used. + /// + /// The syntax identifier. + public StringSyntaxAttribute(string syntax) + { + Syntax = syntax; + Arguments = Array.Empty(); + } + + /// + /// Initializes the with the identifier of the syntax used. + /// + /// The syntax identifier. + /// Optional arguments associated with the specific syntax employed. + public StringSyntaxAttribute( + string syntax, +#if XUNIT_NULLABLE + params object?[] arguments) +#else + params object[] arguments) +#endif + { + Syntax = syntax; + Arguments = arguments; + } + + /// + /// Gets the identifier of the syntax used. + /// + public string Syntax { get; } + + /// + /// Optional arguments associated with the specific syntax employed. + /// +#if XUNIT_NULLABLE + public object?[] Arguments { get; } +#else + public object[] Arguments { get; } +#endif + + /// + /// The syntax identifier for strings containing composite formats for string formatting. + /// + public const string CompositeFormat = nameof(CompositeFormat); + + /// + /// The syntax identifier for strings containing date format specifiers. + /// + public const string DateOnlyFormat = nameof(DateOnlyFormat); + + /// + /// The syntax identifier for strings containing date and time format specifiers. + /// + public const string DateTimeFormat = nameof(DateTimeFormat); + + /// + /// The syntax identifier for strings containing format specifiers. + /// + public const string EnumFormat = nameof(EnumFormat); + + /// + /// The syntax identifier for strings containing format specifiers. + /// + public const string GuidFormat = nameof(GuidFormat); + + /// + /// The syntax identifier for strings containing JavaScript Object Notation (JSON). + /// + public const string Json = nameof(Json); + + /// + /// The syntax identifier for strings containing numeric format specifiers. + /// + public const string NumericFormat = nameof(NumericFormat); + + /// + /// The syntax identifier for strings containing regular expressions. + /// + public const string Regex = nameof(Regex); + + /// + /// The syntax identifier for strings containing time format specifiers. + /// + public const string TimeOnlyFormat = nameof(TimeOnlyFormat); + + /// + /// The syntax identifier for strings containing format specifiers. + /// + public const string TimeSpanFormat = nameof(TimeSpanFormat); + + /// + /// The syntax identifier for strings containing URIs. + /// + public const string Uri = nameof(Uri); + + /// + /// The syntax identifier for strings containing XML. + /// + public const string Xml = nameof(Xml); + } +} + +#endif diff --git a/src/Microsoft.DotNet.XUnitAssert/src/SetAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/SetAsserts.cs new file mode 100644 index 00000000000..6d3596e114b --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/SetAsserts.cs @@ -0,0 +1,300 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System.Collections.Generic; +using System.Collections.Immutable; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that the set contains the given object. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void Contains( + T expected, + ISet set) + { + GuardArgumentNotNull(nameof(set), set); + + // Do not forward to DoesNotContain(expected, set.Keys) as we want the default SDK behavior + if (!set.Contains(expected)) + throw ContainsException.ForSetItemNotFound( + ArgumentFormatter.Format(expected), + CollectionTracker.FormatStart(set) + ); + } + +#if NET8_0_OR_GREATER + + /// + /// Verifies that the read-only set contains the given object. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void Contains( + T expected, + IReadOnlySet set) + { + GuardArgumentNotNull(nameof(set), set); + + // Do not forward to DoesNotContain(expected, set.Keys) as we want the default SDK behavior + if (!set.Contains(expected)) + throw ContainsException.ForSetItemNotFound( + ArgumentFormatter.Format(expected), + CollectionTracker.FormatStart(set) + ); + } + +#endif // NET8_0_OR_GREATER + + /// + /// Verifies that the hashset contains the given object. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void Contains( + T expected, + HashSet set) => + Contains(expected, (ISet)set); + + /// + /// Verifies that the sorted hashset contains the given object. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void Contains( + T expected, + SortedSet set) => + Contains(expected, (ISet)set); + + /// + /// Verifies that the immutable hashset contains the given object. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void Contains( + T expected, + ImmutableHashSet set) => + Contains(expected, (ISet)set); + + /// + /// Verifies that the immutable sorted hashset contains the given object. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void Contains( + T expected, + ImmutableSortedSet set) => + Contains(expected, (ISet)set); + + /// + /// Verifies that the set does not contain the given item. + /// + /// The type of the object to be compared + /// The object that is expected not to be in the set + /// The set to be inspected + /// Thrown when the object is present inside the set + public static void DoesNotContain( + T expected, + ISet set) + { + GuardArgumentNotNull(nameof(set), set); + + if (set.Contains(expected)) + throw DoesNotContainException.ForSetItemFound( + ArgumentFormatter.Format(expected), + CollectionTracker.FormatStart(set) + ); + } + +#if NET8_0_OR_GREATER + + /// + /// Verifies that the read-only set does not contain the given item. + /// + /// The type of the object to be compared + /// The object that is expected not to be in the set + /// The set to be inspected + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + T expected, + IReadOnlySet set) + { + GuardArgumentNotNull(nameof(set), set); + + if (set.Contains(expected)) + throw DoesNotContainException.ForSetItemFound( + ArgumentFormatter.Format(expected), + CollectionTracker.FormatStart(set) + ); + } + +#endif // NET8_0_OR_GREATER + + /// + /// Verifies that the hashset does not contain the given item. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void DoesNotContain( + T expected, + HashSet set) => + DoesNotContain(expected, (ISet)set); + + /// + /// Verifies that the sorted hashset does not contain the given item. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void DoesNotContain( + T expected, + SortedSet set) => + DoesNotContain(expected, (ISet)set); + + /// + /// Verifies that the immutable hashset does not contain the given item. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void DoesNotContain( + T expected, + ImmutableHashSet set) => + DoesNotContain(expected, (ISet)set); + + /// + /// Verifies that the immutable sorted hashset does not contain the given item. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void DoesNotContain( + T expected, + ImmutableSortedSet set) => + DoesNotContain(expected, (ISet)set); + + /// + /// Verifies that a set is a proper subset of another set. + /// + /// The type of the object to be verified + /// The expected subset + /// The set expected to be a proper subset + /// Thrown when the actual set is not a proper subset of the expected set + public static void ProperSubset( + ISet expectedSubset, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expectedSubset), expectedSubset); + + if (actual == null || !actual.IsProperSubsetOf(expectedSubset)) + throw ProperSubsetException.ForFailure( + CollectionTracker.FormatStart(expectedSubset), + actual == null ? "null" : CollectionTracker.FormatStart(actual) + ); + } + + /// + /// Verifies that a set is a proper superset of another set. + /// + /// The type of the object to be verified + /// The expected superset + /// The set expected to be a proper superset + /// Thrown when the actual set is not a proper superset of the expected set + public static void ProperSuperset( + ISet expectedSuperset, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expectedSuperset), expectedSuperset); + + if (actual == null || !actual.IsProperSupersetOf(expectedSuperset)) + throw ProperSupersetException.ForFailure( + CollectionTracker.FormatStart(expectedSuperset), + actual == null ? "null" : CollectionTracker.FormatStart(actual) + ); + } + + /// + /// Verifies that a set is a subset of another set. + /// + /// The type of the object to be verified + /// The expected subset + /// The set expected to be a subset + /// Thrown when the actual set is not a subset of the expected set + public static void Subset( + ISet expectedSubset, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expectedSubset), expectedSubset); + + if (actual == null || !actual.IsSubsetOf(expectedSubset)) + throw SubsetException.ForFailure( + CollectionTracker.FormatStart(expectedSubset), + actual == null ? "null" : CollectionTracker.FormatStart(actual) + ); + } + + /// + /// Verifies that a set is a superset of another set. + /// + /// The type of the object to be verified + /// The expected superset + /// The set expected to be a superset + /// Thrown when the actual set is not a superset of the expected set + public static void Superset( + ISet expectedSuperset, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expectedSuperset), expectedSuperset); + + if (actual == null || !actual.IsSupersetOf(expectedSuperset)) + throw SupersetException.ForFailure( + CollectionTracker.FormatStart(expectedSuperset), + actual == null ? "null" : CollectionTracker.FormatStart(actual) + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/SetAsserts_aot.cs b/src/Microsoft.DotNet.XUnitAssert/src/SetAsserts_aot.cs new file mode 100644 index 00000000000..ed4c23f4837 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/SetAsserts_aot.cs @@ -0,0 +1,336 @@ +#if XUNIT_AOT + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + const string SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED = "Set comparisons with comparison functions are not supported. For more information, see https://xunit.net/docs/hash-sets-vs-linear-containers"; + + /// + /// Verifies that a set contains the same items as the collection. + /// + /// The type of the items in the set + /// The expected items to be in the set + /// The actual set + /// Thrown if the set is not equal + [OverloadResolutionPriority(1)] + public static void Equal( + IEnumerable expected, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expected), expected); + + if (actual != null && actual.SetEquals(expected)) + return; + + SetEqualFailure(expected, actual); + } + + /// + /// Verifies that a set contains the same items as the collection, using + /// the given . + /// + /// The type of the items in the set + /// The expected items to be in the set + /// The actual set + /// The item comparerer to use + /// Thrown if the set is not equal + /// + /// Note that this creates a new hash set with the given comparer, using the items from . + /// Because the comparer may create equality differences from the one was created with, + /// the items in the compared container may differ from the one that was passed, since sets are designed to + /// eliminated duplicate (equal) items. + /// + [OverloadResolutionPriority(1)] + public static void Equal( + IEnumerable expected, +#if XUNIT_NULLABLE + ISet? actual, +#else + ISet actual, +#endif + IEqualityComparer comparer) => + Equal(expected, actual == null ? null : new HashSet(actual, comparer)); + + /// + [Obsolete(SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED, error: true)] + public static void Equal( + IEnumerable expected, +#if XUNIT_NULLABLE + ISet? actual, +#else + ISet actual, +#endif + Func comparer) => + throw new NotSupportedException(SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED); + + /// + /// Verifies that a set contains the same items as the collection. + /// + /// The type of the items in the set + /// The expected items to be in the set + /// The actual set + /// Thrown if the set is not equal + [OverloadResolutionPriority(2)] + public static void Equal( + IEnumerable expected, +#if XUNIT_NULLABLE + IReadOnlySet? actual) +#else + IReadOnlySet actual) +#endif + { + GuardArgumentNotNull(nameof(expected), expected); + + if (actual != null && actual.SetEquals(expected)) + return; + + SetEqualFailure(expected, actual); + } + + /// + /// Verifies that a set contains the same items as the collection, using + /// the given . + /// + /// The type of the items in the set + /// The expected items to be in the set + /// The actual set + /// The item comparerer to use + /// Thrown if the set is not equal + /// + /// Note that this creates a new hash set with the given comparer, using the items from . + /// Because the comparer may create equality differences from the one was created with, + /// the items in the compared container may differ from the one that was passed, since sets are designed to + /// eliminated duplicate (equal) items. + /// + [OverloadResolutionPriority(2)] + public static void Equal( + IEnumerable expected, +#if XUNIT_NULLABLE + IReadOnlySet? actual, +#else + IReadOnlySet actual, +#endif + IEqualityComparer comparer) => + Equal(expected, actual == null ? null : new HashSet(actual, comparer)); + + /// + [Obsolete(SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED, error: true)] + [OverloadResolutionPriority(2)] + public static void Equal( + IEnumerable expected, +#if XUNIT_NULLABLE + IReadOnlySet? actual, +#else + IReadOnlySet actual, +#endif + Func comparer) => + throw new NotSupportedException(SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED); + + /// + /// Verifies that a set does not contain the same items as the collection. + /// + /// The type of the items in the set + /// The expected items to be in the set + /// The actual set + /// Thrown if the set is equal + [OverloadResolutionPriority(1)] + public static void NotEqual( + IEnumerable expected, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expected), expected); + + if (actual != null && !actual.SetEquals(expected)) + return; + + SetNotEqualFailure(expected, actual); + } + + /// + /// Verifies that a set does not contain the same items as the collection, using + /// the given . + /// + /// The type of the items in the set + /// The expected items to be in the set + /// The actual set + /// The item comparerer to use + /// Thrown if the set is equal + /// + /// Note that this creates a new hash set with the given comparer, using the items from . + /// Because the comparer may create equality differences from the one was created with, + /// the items in the compared container may differ from the one that was passed, since sets are designed to + /// eliminated duplicate (equal) items. + /// + [OverloadResolutionPriority(1)] + public static void NotEqual( + IEnumerable expected, +#if XUNIT_NULLABLE + ISet? actual, +#else + ISet actual, +#endif + IEqualityComparer comparer) => + NotEqual(expected, actual == null ? null : new HashSet(actual, comparer)); + + /// + [Obsolete(SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED, error: true)] + [OverloadResolutionPriority(1)] + public static void NotEqual( + IEnumerable expected, +#if XUNIT_NULLABLE + ISet? actual, +#else + ISet actual, +#endif + Func comparer) => + throw new NotSupportedException(SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED); + + /// + /// Verifies that a set does not contain the same items as the collection. + /// + /// The type of the items in the set + /// The expected items to be in the set + /// The actual set + /// Thrown if the set is equal + [OverloadResolutionPriority(2)] + public static void NotEqual( + IEnumerable expected, +#if XUNIT_NULLABLE + IReadOnlySet? actual) +#else + IReadOnlySet actual) +#endif + { + GuardArgumentNotNull(nameof(expected), expected); + + if (actual != null && !actual.SetEquals(expected)) + return; + + SetNotEqualFailure(expected, actual); + } + + /// + /// Verifies that a set does not contain the same items as the collection, using + /// the given . + /// + /// The type of the items in the set + /// The expected items to be in the set + /// The actual set + /// The item comparerer to use + /// Thrown if the set is equal + /// + /// Note that this creates a new hash set with the given comparer, using the items from . + /// Because the comparer may create equality differences from the one was created with, + /// the items in the compared container may differ from the one that was passed, since sets are designed to + /// eliminated duplicate (equal) items. + /// + [OverloadResolutionPriority(2)] + public static void NotEqual( + IEnumerable expected, +#if XUNIT_NULLABLE + IReadOnlySet? actual, +#else + IReadOnlySet actual, +#endif + IEqualityComparer comparer) => + NotEqual(expected, actual == null ? null : new HashSet(actual, comparer)); + + /// + [Obsolete(SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED, error: true)] + [OverloadResolutionPriority(2)] + public static void NotEqual( + IEnumerable expected, +#if XUNIT_NULLABLE + IReadOnlySet? actual, +#else + IReadOnlySet actual, +#endif + Func comparer) => + throw new NotSupportedException(SET_COMPARISON_WITH_FUNC_NOT_SUPPORTED); + + static void SetEqualFailure( + IEnumerable expected, +#if XUNIT_NULLABLE + IEnumerable? actual) +#else + IEnumerable actual) +#endif + { + var expectedFormatted = CollectionTracker.FormatStart(expected); + var actualFormatted = actual == null ? "null" : CollectionTracker.FormatStart(actual); + var expectedTypeFormatted = default(string); + var actualTypeFormatted = default(string); + var expectedType = expected.GetType(); + var actualType = actual?.GetType(); + + if (actualType != null && expectedType != actualType) + { + expectedTypeFormatted = ArgumentFormatter.FormatTypeName(expectedType); + actualTypeFormatted = ArgumentFormatter.FormatTypeName(actualType); + } + + var expectedTypeDefinition = SafeGetGenericTypeDefinition(expectedType); + var actualTypeDefinition = SafeGetGenericTypeDefinition(actualType); + var collectionDisplay = + expectedTypeDefinition == typeofHashSet && actualTypeDefinition == typeofHashSet + ? "HashSets" + : "Sets"; + + throw EqualException.ForMismatchedSets(expectedFormatted, expectedTypeFormatted, actualFormatted, actualTypeFormatted, collectionDisplay); + } + + static void SetNotEqualFailure( + IEnumerable expected, +#if XUNIT_NULLABLE + IEnumerable? actual) +#else + IEnumerable actual) +#endif + { + var expectedFormatted = CollectionTracker.FormatStart(expected); + var actualFormatted = actual == null ? "null" : CollectionTracker.FormatStart(actual); + var expectedTypeFormatted = default(string); + var actualTypeFormatted = default(string); + var expectedType = expected.GetType(); + var actualType = actual?.GetType(); + + if (actualType != null && expectedType != actualType) + { + expectedTypeFormatted = ArgumentFormatter.FormatTypeName(expectedType); + actualTypeFormatted = ArgumentFormatter.FormatTypeName(actualType); + } + + var expectedTypeDefinition = SafeGetGenericTypeDefinition(expectedType); + var actualTypeDefinition = SafeGetGenericTypeDefinition(actualType); + var collectionDisplay = + expectedTypeDefinition == typeofHashSet && actualTypeDefinition == typeofHashSet + ? "HashSets" + : "Sets"; + + throw NotEqualException.ForEqualSets(expectedFormatted, expectedTypeFormatted, actualFormatted, actualTypeFormatted, collectionDisplay); + } + } +} + +#endif // !XUNIT_AOT diff --git a/src/Microsoft.DotNet.XUnitAssert/src/SkipAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/SkipAsserts.cs new file mode 100644 index 00000000000..42acf115183 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/SkipAsserts.cs @@ -0,0 +1,72 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ + partial class Assert + { + /// + /// Skips the current test. Used when determining whether a test should be skipped + /// happens at runtime rather than at discovery time. + /// + /// The message to indicate why the test was skipped +#if XUNIT_NULLABLE + [DoesNotReturn] +#endif + public static void Skip(string reason) + { + GuardArgumentNotNull(nameof(reason), reason); + + throw SkipException.ForSkip(reason); + } + + /// + /// Will skip the current test unless evaluates to . + /// + /// When , the test will continue to run; when , + /// the test will be skipped + /// The message to indicate why the test was skipped + public static void SkipUnless( +#if XUNIT_NULLABLE + [DoesNotReturnIf(false)] bool condition, +#else + bool condition, +#endif + string reason) + { + GuardArgumentNotNull(nameof(reason), reason); + + if (!condition) + throw SkipException.ForSkip(reason); + } + + /// + /// Will skip the current test when evaluates to . + /// + /// When , the test will be skipped; when , + /// the test will continue to run + /// The message to indicate why the test was skipped + public static void SkipWhen( +#if XUNIT_NULLABLE + [DoesNotReturnIf(true)] bool condition, +#else + bool condition, +#endif + string reason) + { + GuardArgumentNotNull(nameof(reason), reason); + + if (condition) + throw SkipException.ForSkip(reason); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/SpanAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/SpanAsserts.cs new file mode 100644 index 00000000000..38bfb812265 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/SpanAsserts.cs @@ -0,0 +1,201 @@ +#pragma warning disable CA1052 // Static holder types should be static + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + // While there is an implicit conversion operator from Span to ReadOnlySpan, the + // compiler still stumbles to do this automatically, which means we end up with lots of overloads + // with various arrangements of Span and ReadOnlySpan. + + // Also note that these classes will convert nulls into empty arrays automatically, since there + // is no way to represent a null readonly struct. + + /// + /// Verifies that a span contains a given sub-span + /// + /// The sub-span expected to be in the span + /// The span to be inspected + /// Thrown when the sub-span is not present inside the span + public static void Contains( + Span expectedSubSpan, + Span actualSpan) + where T : IEquatable => + Contains((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan); + + /// + /// Verifies that a span contains a given sub-span + /// + /// The sub-span expected to be in the span + /// The span to be inspected + /// Thrown when the sub-span is not present inside the span + public static void Contains( + Span expectedSubSpan, + ReadOnlySpan actualSpan) + where T : IEquatable => + Contains((ReadOnlySpan)expectedSubSpan, actualSpan); + + /// + /// Verifies that a span contains a given sub-span + /// + /// The sub-span expected to be in the span + /// The span to be inspected + /// Thrown when the sub-span is not present inside the span + public static void Contains( + ReadOnlySpan expectedSubSpan, + Span actualSpan) + where T : IEquatable => + Contains(expectedSubSpan, (ReadOnlySpan)actualSpan); + + /// + /// Verifies that a span contains a given sub-span + /// + /// The sub-span expected to be in the span + /// The span to be inspected + /// Thrown when the sub-span is not present inside the span + public static void Contains( + ReadOnlySpan expectedSubSpan, + ReadOnlySpan actualSpan) + where T : IEquatable + { + if (actualSpan.IndexOf(expectedSubSpan) < 0) + throw ContainsException.ForSubSpanNotFound( + CollectionTracker.FormatStart(expectedSubSpan), + CollectionTracker.FormatStart(actualSpan) + ); + } + + /// + /// Verifies that a span does not contain a given sub-span + /// + /// The sub-span expected not to be in the span + /// The span to be inspected + /// Thrown when the sub-span is present inside the span + public static void DoesNotContain( + Span expectedSubSpan, + Span actualSpan) + where T : IEquatable => + DoesNotContain((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan); + + /// + /// Verifies that a span does not contain a given sub-span + /// + /// The sub-span expected not to be in the span + /// The span to be inspected + /// Thrown when the sub-span is present inside the span + public static void DoesNotContain( + Span expectedSubSpan, + ReadOnlySpan actualSpan) + where T : IEquatable => + DoesNotContain((ReadOnlySpan)expectedSubSpan, actualSpan); + + /// + /// Verifies that a span does not contain a given sub-span + /// + /// The sub-span expected not to be in the span + /// The span to be inspected + /// Thrown when the sub-span is present inside the span + public static void DoesNotContain( + ReadOnlySpan expectedSubSpan, + Span actualSpan) + where T : IEquatable => + DoesNotContain(expectedSubSpan, (ReadOnlySpan)actualSpan); + + /// + /// Verifies that a span does not contain a given sub-span + /// + /// The sub-span expected not to be in the span + /// The span to be inspected + /// Thrown when the sub-span is present inside the span + public static void DoesNotContain( + ReadOnlySpan expectedSubSpan, + ReadOnlySpan actualSpan) + where T : IEquatable + { + var idx = actualSpan.IndexOf(expectedSubSpan); + if (idx > -1) + { + var formattedExpected = CollectionTracker.FormatStart(expectedSubSpan); + var formattedActual = CollectionTracker.FormatIndexedMismatch(actualSpan, idx, out var failurePointerIndent); + + throw DoesNotContainException.ForSubSpanFound( + formattedExpected, + idx, + failurePointerIndent, + formattedActual + ); + } + } + + /// + /// Verifies that a span and an array contain the same values in the same order. + /// + /// The expected span value. + /// The actual array value. + /// Thrown when the collections are not equal. + // This overload exists per https://github.com/xunit/xunit/discussions/3021 + public static void Equal( + ReadOnlySpan expectedSpan, + T[] actualArray) + where T : IEquatable => + Equal(expectedSpan, actualArray.AsSpan()); + + /// + /// Verifies that two spans contain the same values in the same order. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equal. + public static void Equal( + Span expectedSpan, + Span actualSpan) + where T : IEquatable => + Equal((ReadOnlySpan)expectedSpan, (ReadOnlySpan)actualSpan); + + /// + /// Verifies that two spans contain the same values in the same order. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equal. + public static void Equal( + Span expectedSpan, + ReadOnlySpan actualSpan) + where T : IEquatable => + Equal((ReadOnlySpan)expectedSpan, actualSpan); + + /// + /// Verifies that two spans contain the same values in the same order. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equal. + public static void Equal( + ReadOnlySpan expectedSpan, + Span actualSpan) + where T : IEquatable => + Equal(expectedSpan, (ReadOnlySpan)actualSpan); + + /// + /// Verifies that two spans contain the same values in the same order. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equal. + public static void Equal( + ReadOnlySpan expectedSpan, + ReadOnlySpan actualSpan) + where T : IEquatable + { + if (!expectedSpan.SequenceEqual(actualSpan)) + Equal(expectedSpan.ToArray(), actualSpan.ToArray(), new AssertEqualityComparer()); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/StringAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/StringAsserts.cs new file mode 100644 index 00000000000..c6a806224be --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/StringAsserts.cs @@ -0,0 +1,1634 @@ +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0090 // Use 'new(...)' + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#endif + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using Xunit.Internal; +using Xunit.Sdk; + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that a string contains a given sub-string, using the current culture. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// Thrown when the sub-string is not present inside the string + public static void Contains( + string expectedSubstring, +#if XUNIT_NULLABLE + string? actualString) => +#else + string actualString) => +#endif + Contains(expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is not present inside the string + public static void Contains( + string expectedSubstring, +#if XUNIT_NULLABLE + string? actualString, +#else + string actualString, +#endif + StringComparison comparisonType) + { + GuardArgumentNotNull(nameof(expectedSubstring), expectedSubstring); + + if (actualString == null || actualString.IndexOf(expectedSubstring, comparisonType) < 0) + throw ContainsException.ForSubStringNotFound(expectedSubstring, actualString); + } + + /// + /// Verifies that a string contains a given sub-string, using the current culture. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// Thrown when the sub-string is not present inside the string + public static void Contains( + Memory expectedSubstring, + Memory actualString) => + Contains((ReadOnlyMemory)expectedSubstring, (ReadOnlyMemory)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given sub-string, using the current culture. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// Thrown when the sub-string is not present inside the string + public static void Contains( + Memory expectedSubstring, + ReadOnlyMemory actualString) => + Contains((ReadOnlyMemory)expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given sub-string, using the current culture. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// Thrown when the sub-string is not present inside the string + public static void Contains( + ReadOnlyMemory expectedSubstring, + Memory actualString) => + Contains(expectedSubstring, (ReadOnlyMemory)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given sub-string, using the current culture. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// Thrown when the sub-string is not present inside the string + public static void Contains( + ReadOnlyMemory expectedSubstring, + ReadOnlyMemory actualString) => + Contains(expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is not present inside the string + public static void Contains( + Memory expectedSubstring, + Memory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlyMemory)expectedSubstring, (ReadOnlyMemory)actualString, comparisonType); + + /// + /// Verifies that a string contains a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is not present inside the string + public static void Contains( + Memory expectedSubstring, + ReadOnlyMemory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlyMemory)expectedSubstring, actualString, comparisonType); + + /// + /// Verifies that a string contains a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is not present inside the string + public static void Contains( + ReadOnlyMemory expectedSubstring, + Memory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains(expectedSubstring, (ReadOnlyMemory)actualString, comparisonType); + + /// + /// Verifies that a string contains a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is not present inside the string + public static void Contains( + ReadOnlyMemory expectedSubstring, + ReadOnlyMemory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + GuardArgumentNotNull(nameof(expectedSubstring), expectedSubstring); + + Contains(expectedSubstring.Span, actualString.Span, comparisonType); + } + + /// + /// Verifies that a string contains a given string, using the given comparison type. + /// + /// The string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string is not present inside the string + public static void Contains( + Span expectedSubstring, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string contains a given string, using the given comparison type. + /// + /// The string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string is not present inside the string + public static void Contains( + Span expectedSubstring, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlySpan)expectedSubstring, actualString, comparisonType); + + /// + /// Verifies that a string contains a given string, using the given comparison type. + /// + /// The string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string is not present inside the string + public static void Contains( + ReadOnlySpan expectedSubstring, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains(expectedSubstring, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string contains a given string, using the given comparison type. + /// + /// The string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string is not present inside the string + public static void Contains( + ReadOnlySpan expectedSubstring, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (actualString.IndexOf(expectedSubstring, comparisonType) < 0) + throw ContainsException.ForSubStringNotFound( + expectedSubstring.ToString(), + actualString.ToString() + ); + } + + /// + /// Verifies that a string contains a given string, using the current culture. + /// + /// The string expected to be in the string + /// The string to be inspected + /// Thrown when the string is not present inside the string + public static void Contains( + Span expectedSubstring, + Span actualString) => + Contains((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given string, using the current culture. + /// + /// The string expected to be in the string + /// The string to be inspected + /// Thrown when the string is not present inside the string + public static void Contains( + Span expectedSubstring, + ReadOnlySpan actualString) => + Contains((ReadOnlySpan)expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given string, using the current culture. + /// + /// The string expected to be in the string + /// The string to be inspected + /// Thrown when the string is not present inside the string + public static void Contains( + ReadOnlySpan expectedSubstring, + Span actualString) => + Contains(expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given string, using the current culture. + /// + /// The string expected to be in the string + /// The string to be inspected + /// Thrown when the string is not present inside the string + public static void Contains( + ReadOnlySpan expectedSubstring, + ReadOnlySpan actualString) => + Contains(expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + string expectedSubstring, +#if XUNIT_NULLABLE + string? actualString) => +#else + string actualString) => +#endif + DoesNotContain(expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + string expectedSubstring, +#if XUNIT_NULLABLE + string? actualString, +#else + string actualString, +#endif + StringComparison comparisonType) + { + GuardArgumentNotNull(nameof(expectedSubstring), expectedSubstring); + + if (actualString != null) + { + var idx = actualString.IndexOf(expectedSubstring, comparisonType); + if (idx >= 0) + throw DoesNotContainException.ForSubStringFound(expectedSubstring, idx, actualString); + } + } + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Memory expectedSubstring, + Memory actualString) => + DoesNotContain((ReadOnlyMemory)expectedSubstring, (ReadOnlyMemory)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Memory expectedSubstring, + ReadOnlyMemory actualString) => + DoesNotContain((ReadOnlyMemory)expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlyMemory expectedSubstring, + Memory actualString) => + DoesNotContain(expectedSubstring, (ReadOnlyMemory)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlyMemory expectedSubstring, + ReadOnlyMemory actualString) => + DoesNotContain(expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Memory expectedSubstring, + Memory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlyMemory)expectedSubstring, (ReadOnlyMemory)actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Memory expectedSubstring, + ReadOnlyMemory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlyMemory)expectedSubstring, actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlyMemory expectedSubstring, + Memory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain(expectedSubstring, (ReadOnlyMemory)actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlyMemory expectedSubstring, + ReadOnlyMemory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + GuardArgumentNotNull(nameof(expectedSubstring), expectedSubstring); + + DoesNotContain(expectedSubstring.Span, actualString.Span, comparisonType); + } + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Span expectedSubstring, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Span expectedSubstring, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlySpan)expectedSubstring, actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlySpan expectedSubstring, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain(expectedSubstring, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlySpan expectedSubstring, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + var idx = actualString.IndexOf(expectedSubstring, comparisonType); + if (idx > -1) + throw DoesNotContainException.ForSubStringFound(expectedSubstring.ToString(), idx, actualString.ToString()); + } + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Span expectedSubstring, + Span actualString) => + DoesNotContain((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Span expectedSubstring, + ReadOnlySpan actualString) => + DoesNotContain((ReadOnlySpan)expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlySpan expectedSubstring, + Span actualString) => + DoesNotContain(expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlySpan expectedSubstring, + ReadOnlySpan actualString) => + DoesNotContain(expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not match a regular expression. + /// + /// The regex pattern expected not to match + /// The string to be inspected + /// Thrown when the string matches the regex pattern + public static void DoesNotMatch( + [StringSyntax(StringSyntaxAttribute.Regex)] + string expectedRegexPattern, +#if XUNIT_NULLABLE + string? actualString) +#else + string actualString) +#endif + { + GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern); + + if (actualString != null) + { + var match = Regex.Match(actualString, expectedRegexPattern); + if (match.Success) + { + var formattedExpected = AssertHelper.ShortenAndEncodeString(expectedRegexPattern); + var formattedActual = AssertHelper.ShortenAndEncodeString(actualString, match.Index, out var pointerIndent); + + throw DoesNotMatchException.ForMatch(formattedExpected, match.Index, pointerIndent, formattedActual); + } + } + } + + /// + /// Verifies that a string does not match a regular expression. + /// + /// The regex expected not to match + /// The string to be inspected + /// Thrown when the string matches the regex + public static void DoesNotMatch( + Regex expectedRegex, +#if XUNIT_NULLABLE + string? actualString) +#else + string actualString) +#endif + { + GuardArgumentNotNull(nameof(expectedRegex), expectedRegex); + + if (actualString != null) + { + var match = expectedRegex.Match(actualString); + if (match.Success) + { + var formattedExpected = AssertHelper.ShortenAndEncodeString(expectedRegex.ToString()); + var formattedActual = AssertHelper.ShortenAndEncodeString(actualString, match.Index, out var pointerIndent); + + throw DoesNotMatchException.ForMatch(formattedExpected, match.Index, pointerIndent, formattedActual); + } + } + } + + /// + /// Verifies that a string is empty. + /// + /// The string value to be inspected + /// Thrown when the string is null + /// Thrown when the string is not empty + public static void Empty(string value) + { + GuardArgumentNotNull(nameof(value), value); + + if (value.Length != 0) + throw EmptyException.ForNonEmptyString(value); + } + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( +#if XUNIT_NULLABLE + string? expectedEndString, + string? actualString) => +#else + string expectedEndString, + string actualString) => +#endif + EndsWith(expectedEndString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( +#if XUNIT_NULLABLE + string? expectedEndString, + string? actualString, +#else + string expectedEndString, + string actualString, +#endif + StringComparison comparisonType) + { + if (expectedEndString == null || actualString == null || !actualString.EndsWith(expectedEndString, comparisonType)) + throw EndsWithException.ForStringNotFound(expectedEndString, actualString); + } + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + Memory expectedEndString, + Memory actualString) => + EndsWith((ReadOnlyMemory)expectedEndString, (ReadOnlyMemory)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + Memory expectedEndString, + ReadOnlyMemory actualString) => + EndsWith((ReadOnlyMemory)expectedEndString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + ReadOnlyMemory expectedEndString, + Memory actualString) => + EndsWith(expectedEndString, (ReadOnlyMemory)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + ReadOnlyMemory expectedEndString, + ReadOnlyMemory actualString) => + EndsWith(expectedEndString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + Memory expectedEndString, + Memory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlyMemory)expectedEndString, (ReadOnlyMemory)actualString, comparisonType); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + Memory expectedEndString, + ReadOnlyMemory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlyMemory)expectedEndString, actualString, comparisonType); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + ReadOnlyMemory expectedEndString, + Memory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith(expectedEndString, (ReadOnlyMemory)actualString, comparisonType); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + ReadOnlyMemory expectedEndString, + ReadOnlyMemory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + GuardArgumentNotNull(nameof(expectedEndString), expectedEndString); + + EndsWith(expectedEndString.Span, actualString.Span, comparisonType); + } + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + Span expectedEndString, + Span actualString) => + EndsWith((ReadOnlySpan)expectedEndString, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + Span expectedEndString, + ReadOnlySpan actualString) => + EndsWith((ReadOnlySpan)expectedEndString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + ReadOnlySpan expectedEndString, + Span actualString) => + EndsWith(expectedEndString, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + ReadOnlySpan expectedEndString, + ReadOnlySpan actualString) => + EndsWith(expectedEndString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + Span expectedEndString, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlySpan)expectedEndString, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + Span expectedEndString, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlySpan)expectedEndString, actualString, comparisonType); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + ReadOnlySpan expectedEndString, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith(expectedEndString, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected sub-string + public static void EndsWith( + ReadOnlySpan expectedEndString, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (!actualString.EndsWith(expectedEndString, comparisonType)) + throw EndsWithException.ForStringNotFound(expectedEndString.ToString(), actualString.ToString()); + } + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( +#if XUNIT_NULLABLE + string? expected, + string? actual) => +#else + string expected, + string actual) => +#endif + Equal(expected, actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + + public static void Equal( + ReadOnlySpan expected, + ReadOnlySpan actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) + { + var result = StringAssertEqualityComparer.Equivalent(expected, actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + if (!result.Equal) + throw EqualException.ForMismatchedStrings(expected.ToString(), actual.ToString(), result.MismatchIndexX ?? -1, result.MismatchIndexY ?? -1); + } + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + Memory expected, + Memory actual) => + Equal((ReadOnlyMemory)expected, (ReadOnlyMemory)actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + Memory expected, + ReadOnlyMemory actual) => + Equal((ReadOnlyMemory)expected, actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + ReadOnlyMemory expected, + Memory actual) => + Equal(expected, (ReadOnlyMemory)actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + ReadOnlyMemory expected, + ReadOnlyMemory actual) => + Equal(expected, actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( + Memory expected, + Memory actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal((ReadOnlyMemory)expected, (ReadOnlyMemory)actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( + Memory expected, + ReadOnlyMemory actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal((ReadOnlyMemory)expected, actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( + ReadOnlyMemory expected, + Memory actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal(expected, (ReadOnlyMemory)actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( + ReadOnlyMemory expected, + ReadOnlyMemory actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) + { + GuardArgumentNotNull(nameof(expected), expected); + + Equal( + expected.Span, + actual.Span, + ignoreCase, + ignoreLineEndingDifferences, + ignoreWhiteSpaceDifferences, + ignoreAllWhiteSpace + ); + } + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + Span expected, + Span actual) => + Equal((ReadOnlySpan)expected, (ReadOnlySpan)actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + Span expected, + ReadOnlySpan actual) => + Equal((ReadOnlySpan)expected, actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + ReadOnlySpan expected, + Span actual) => + Equal(expected, (ReadOnlySpan)actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + ReadOnlySpan expected, + ReadOnlySpan actual) => + Equal(expected, actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to , ignores all white space differences during comparison. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( + Span expected, + Span actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal((ReadOnlySpan)expected, (ReadOnlySpan)actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to , ignores all white space differences during comparison. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( + Span expected, + ReadOnlySpan actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal((ReadOnlySpan)expected, actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to , removes all whitespaces and tabs before comparing. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( + ReadOnlySpan expected, + Span actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal(expected, (ReadOnlySpan)actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to , ignores cases differences. The invariant culture is used. + /// If set to , treats \r\n, \r, and \n as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to , treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( +#if XUNIT_NULLABLE + string? expected, + string? actual, +#else + string expected, + string actual, +#endif + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) + { + if (expected == null && actual == null) + return; + if (expected == null || actual == null) + throw EqualException.ForMismatchedStrings(expected, actual, -1, -1); + + Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + } + + /// + /// Verifies that a string matches a regular expression. + /// + /// The regex pattern expected to match + /// The string to be inspected + /// Thrown when the string does not match the regex pattern + public static void Matches( + [StringSyntax(StringSyntaxAttribute.Regex)] + string expectedRegexPattern, +#if XUNIT_NULLABLE + string? actualString) +#else + string actualString) +#endif + { + GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern); + + if (actualString == null || !Regex.IsMatch(actualString, expectedRegexPattern)) + throw MatchesException.ForMatchNotFound(expectedRegexPattern, actualString); + } + + /// + /// Verifies that a string matches a regular expression. + /// + /// The regex expected to match + /// The string to be inspected + /// Thrown when the string does not match the regex + public static void Matches( + Regex expectedRegex, +#if XUNIT_NULLABLE + string? actualString) +#else + string actualString) +#endif + { + GuardArgumentNotNull(nameof(expectedRegex), expectedRegex); + + if (actualString == null || !expectedRegex.IsMatch(actualString)) + throw MatchesException.ForMatchNotFound(expectedRegex.ToString(), actualString); + } + + /// + /// Verifies that a string starts with a given string, using the current culture. + /// + /// The string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( +#if XUNIT_NULLABLE + string? expectedStartString, + string? actualString) => +#else + string expectedStartString, + string actualString) => +#endif + StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( +#if XUNIT_NULLABLE + string? expectedStartString, + string? actualString, +#else + string expectedStartString, + string actualString, +#endif + StringComparison comparisonType) + { + if (expectedStartString == null || actualString == null || !actualString.StartsWith(expectedStartString, comparisonType)) + throw StartsWithException.ForStringNotFound(expectedStartString, actualString); + } + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + Memory expectedStartString, + Memory actualString) => + StartsWith((ReadOnlyMemory)expectedStartString, (ReadOnlyMemory)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + Memory expectedStartString, + ReadOnlyMemory actualString) => + StartsWith((ReadOnlyMemory)expectedStartString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + ReadOnlyMemory expectedStartString, + Memory actualString) => + StartsWith(expectedStartString, (ReadOnlyMemory)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + ReadOnlyMemory expectedStartString, + ReadOnlyMemory actualString) => + StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + Memory expectedStartString, + Memory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlyMemory)expectedStartString, (ReadOnlyMemory)actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + Memory expectedStartString, + ReadOnlyMemory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlyMemory)expectedStartString, actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + ReadOnlyMemory expectedStartString, + Memory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith(expectedStartString, (ReadOnlyMemory)actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + ReadOnlyMemory expectedStartString, + ReadOnlyMemory actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + GuardArgumentNotNull(nameof(expectedStartString), expectedStartString); + + StartsWith(expectedStartString.Span, actualString.Span, comparisonType); + } + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + Span expectedStartString, + Span actualString) => + StartsWith((ReadOnlySpan)expectedStartString, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + Span expectedStartString, + ReadOnlySpan actualString) => + StartsWith((ReadOnlySpan)expectedStartString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + ReadOnlySpan expectedStartString, + Span actualString) => + StartsWith(expectedStartString, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + ReadOnlySpan expectedStartString, + ReadOnlySpan actualString) => + StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + Span expectedStartString, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlySpan)expectedStartString, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + Span expectedStartString, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlySpan)expectedStartString, actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + ReadOnlySpan expectedStartString, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith(expectedStartString, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected sub-string + public static void StartsWith( + ReadOnlySpan expectedStartString, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (!actualString.StartsWith(expectedStartString, comparisonType)) + throw StartsWithException.ForStringNotFound(expectedStartString.ToString(), actualString.ToString()); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/TypeAsserts.cs b/src/Microsoft.DotNet.XUnitAssert/src/TypeAsserts.cs new file mode 100644 index 00000000000..342dcd0be87 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/src/TypeAsserts.cs @@ -0,0 +1,291 @@ +#pragma warning disable CA1052 // Static holder types should be static +#pragma warning disable CA1720 // Identifier contains type name + +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 +#pragma warning disable CS8625 +#endif + +using System; +using System.Globalization; +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ + partial class Assert + { + /// + /// Verifies that an object is of the given type or a derived type. + /// + /// The type the object should be + /// The object to be evaluated + /// The object, casted to type T when successful + /// Thrown when the object is not the given type +#if XUNIT_NULLABLE + public static T IsAssignableFrom(object? @object) +#else + public static T IsAssignableFrom(object @object) +#endif + { +#pragma warning disable xUnit2007 + IsAssignableFrom(typeof(T), @object); +#pragma warning restore xUnit2007 + return (T)@object; + } + + /// + /// Verifies that an object is of the given type or a derived type. + /// + /// The type the object should be + /// The object to be evaluated + /// Thrown when the object is not the given type + public static void IsAssignableFrom( + Type expectedType, +#if XUNIT_NULLABLE + [NotNull] object? @object) +#else + object @object) +#endif + { + GuardArgumentNotNull(nameof(expectedType), expectedType); + + if (@object == null || !expectedType.IsAssignableFrom(@object.GetType())) + throw IsAssignableFromException.ForIncompatibleType(expectedType, @object); + } + + /// + /// Verifies that an object is not of the given type or a derived type. + /// + /// The type the object should not be + /// The object to be evaluated + /// The object, casted to type T when successful + /// Thrown when the object is of the given type +#if XUNIT_NULLABLE + public static void IsNotAssignableFrom(object? @object) => +#else + public static void IsNotAssignableFrom(object @object) => +#endif + IsNotAssignableFrom(typeof(T), @object); + + /// + /// Verifies that an object is not of the given type or a derived type. + /// + /// The type the object should not be + /// The object to be evaluated + /// Thrown when the object is of the given type + public static void IsNotAssignableFrom( + Type expectedType, +#if XUNIT_NULLABLE + object? @object) +#else + object @object) +#endif + { + GuardArgumentNotNull(nameof(expectedType), expectedType); + + if (@object != null && expectedType.IsAssignableFrom(@object.GetType())) + throw IsNotAssignableFromException.ForCompatibleType(expectedType, @object); + } + + /// + /// Verifies that an object is not exactly the given type. + /// + /// The type the object should not be + /// The object to be evaluated + /// Thrown when the object is the given type +#if XUNIT_NULLABLE + public static void IsNotType(object? @object) => +#else + public static void IsNotType(object @object) => +#endif +#pragma warning disable xUnit2007 + IsNotType(typeof(T), @object); +#pragma warning restore xUnit2007 + + /// + /// Verifies that an object is not of the given type. + /// + /// The type the object should not be + /// The object to be evaluated + /// Will only fail with an exact type match when is + /// passed; will fail with a compatible type match when is passed. + /// Thrown when the object is the given type +#if XUNIT_NULLABLE + public static void IsNotType( + object? @object, +#else + public static void IsNotType( + object @object, +#endif + bool exactMatch) => +#pragma warning disable xUnit2007 + IsNotType(typeof(T), @object, exactMatch); +#pragma warning restore xUnit2007 + + /// + /// Verifies that an object is not exactly the given type. + /// + /// The type the object should not be + /// The object to be evaluated + /// Thrown when the object is the given type + public static void IsNotType( + Type expectedType, +#if XUNIT_NULLABLE + object? @object) => +#else + object @object) => +#endif + IsNotType(expectedType, @object, exactMatch: true); + + /// + /// Verifies that an object is not of the given type. + /// + /// The type the object should not be + /// The object to be evaluated + /// Will only fail with an exact type match when is + /// passed; will fail with a compatible type match when is passed. + /// Thrown when the object is the given type + public static void IsNotType( + Type expectedType, +#if XUNIT_NULLABLE + object? @object, +#else + object @object, +#endif + bool exactMatch) + { + GuardArgumentNotNull(nameof(expectedType), expectedType); + + if (exactMatch) + { + if (@object != null && expectedType.Equals(@object.GetType())) + throw IsNotTypeException.ForExactType(expectedType); + } + else + { + var actualType = @object?.GetType(); + + if (actualType != null && expectedType.IsAssignableFrom(actualType)) + throw IsNotTypeException.ForCompatibleType(expectedType, actualType); + } + } + + /// + /// Verifies that an object is exactly the given type (and not a derived type). + /// + /// The type the object should be + /// The object to be evaluated + /// The object, casted to type T when successful + /// Thrown when the object is not the given type +#if XUNIT_NULLABLE + public static T IsType([NotNull] object? @object) +#else + public static T IsType(object @object) +#endif + { +#pragma warning disable CA2263 +#pragma warning disable xUnit2007 + IsType(typeof(T), @object, exactMatch: true); +#pragma warning restore xUnit2007 +#pragma warning restore CA2263 + return (T)@object; + } + + /// + /// Verifies that an object of is the given type. + /// + /// The type the object should be + /// The object to be evaluated + /// Will only pass with an exact type match when is + /// passed; will pass with a compatible type match when is passed. + /// The object, casted to type T when successful + /// Thrown when the object is not the given type +#if XUNIT_NULLABLE + public static T IsType( + [NotNull] object? @object, +#else + public static T IsType( + object @object, +#endif + bool exactMatch) + { +#pragma warning disable xUnit2007 + IsType(typeof(T), @object, exactMatch); +#pragma warning restore xUnit2007 + return (T)@object; + } + + /// + /// Verifies that an object is exactly the given type (and not a derived type). + /// + /// The type the object should be + /// The object to be evaluated + /// Thrown when the object is not the given type + public static void IsType( + Type expectedType, +#if XUNIT_NULLABLE + [NotNull] object? @object) => +#else + object @object) => +#endif + IsType(expectedType, @object, exactMatch: true); + + /// + /// Verifies that an object is of the given type. + /// + /// The type the object should be + /// The object to be evaluated + /// Will only pass with an exact type match when is + /// passed; will pass with a compatible type match when is passed. + /// Thrown when the object is not the given type + public static void IsType( + Type expectedType, +#if XUNIT_NULLABLE + [NotNull] object? @object, +#else + object @object, +#endif + bool exactMatch) + { + GuardArgumentNotNull(nameof(expectedType), expectedType); + + if (@object == null) + { + if (exactMatch) + throw IsTypeException.ForMismatchedType(ArgumentFormatter.Format(expectedType), null); + else + throw IsTypeException.ForIncompatibleType(ArgumentFormatter.Format(expectedType), null); + } + + var actualType = @object.GetType(); + var compatible = + exactMatch + ? expectedType == actualType + : expectedType.IsAssignableFrom(actualType); + + if (!compatible) + { + var expectedTypeName = ArgumentFormatter.Format(expectedType); + var actualTypeName = ArgumentFormatter.Format(actualType); + + if (expectedTypeName == actualTypeName) + { + expectedTypeName += string.Format(CultureInfo.CurrentCulture, " (from {0})", expectedType.Assembly.GetName().FullName); + actualTypeName += string.Format(CultureInfo.CurrentCulture, " (from {0})", actualType.Assembly.GetName().FullName); + } + + if (exactMatch) + throw IsTypeException.ForMismatchedType(expectedTypeName, actualTypeName); + else + throw IsTypeException.ForIncompatibleType(expectedTypeName, actualTypeName); + } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/src/xunit.snk b/src/Microsoft.DotNet.XUnitAssert/src/xunit.snk new file mode 100644 index 00000000000..93641b9761c Binary files /dev/null and b/src/Microsoft.DotNet.XUnitAssert/src/xunit.snk differ diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/AsyncCollectionAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/AsyncCollectionAssertsTests.cs new file mode 100644 index 00000000000..adc417d6e93 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/AsyncCollectionAssertsTests.cs @@ -0,0 +1,1621 @@ +#if NET8_0_OR_GREATER + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Sdk; + +public class AsyncCollectionAssertsTests +{ + public class All + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.All(default(IAsyncEnumerable)!, _ => { })); + Assert.Throws("action", () => Assert.All(Array.Empty().ToAsyncEnumerable(), (Action)null!)); + Assert.Throws("action", () => Assert.All(Array.Empty().ToAsyncEnumerable(), (Action)null!)); + } + + [Fact] + public static void Success() + { + var items = new[] { 1, 1, 1, 1, 1, 1 }.ToAsyncEnumerable(); + + Assert.All(items, x => Assert.Equal(1, x)); + } + + [Fact] + public static void Failure() + { + var items = new[] { 1, 1, 42, 2112, 1, 1 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.All(items, item => Assert.Equal(1, item))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.All() Failure: 2 out of 6 items in the collection did not pass." + Environment.NewLine + + "[2]: Item: 42" + Environment.NewLine + + " Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 1" + Environment.NewLine + + " Actual: 42" + Environment.NewLine + + "[3]: Item: 2112" + Environment.NewLine + + " Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 1" + Environment.NewLine + + " Actual: 2112", + ex.Message + ); + } + + [Fact] + public static void ActionCanReceiveIndex() + { + var items = new[] { 1, 1, 2, 2, 1, 1 }; + var indices = new List(); + + Assert.All(items, (_, idx) => indices.Add(idx)); + + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, indices); + } + } + + public class AllAsync + { + [Fact] + public static async Task GuardClauses() + { + await Assert.ThrowsAsync("collection", () => Assert.AllAsync(default(IAsyncEnumerable)!, async _ => await Task.Yield())); + await Assert.ThrowsAsync("action", () => Assert.AllAsync(Array.Empty().ToAsyncEnumerable(), (Func)null!)); + await Assert.ThrowsAsync("action", () => Assert.AllAsync(Array.Empty().ToAsyncEnumerable(), (Func)null!)); + } + + [Fact] + public static async Task Success() + { + var items = new[] { 1, 1, 1, 1, 1, 1 }.ToAsyncEnumerable(); + + await Assert.AllAsync(items, async item => { await Task.Yield(); Assert.Equal(1, item); }); + } + + [Fact] + public static void Failure() + { + var items = new[] { 1, 1, 42, 2112, 1, 1 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.All(items, x => Assert.Equal(1, x))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.All() Failure: 2 out of 6 items in the collection did not pass." + Environment.NewLine + + "[2]: Item: 42" + Environment.NewLine + + " Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 1" + Environment.NewLine + + " Actual: 42" + Environment.NewLine + + "[3]: Item: 2112" + Environment.NewLine + + " Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 1" + Environment.NewLine + + " Actual: 2112", + ex.Message + ); + } + + [Fact] + public static async Task ActionCanReceiveIndex() + { + var items = new[] { 1, 1, 2, 2, 1, 1 }.ToAsyncEnumerable(); + var indices = new List(); + + await Assert.AllAsync(items, async (_, idx) => { await Task.Yield(); indices.Add(idx); }); + + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, indices); + } + } + + public class Collection + { + [Fact] + public static void EmptyCollection() + { + var list = new List().ToAsyncEnumerable(); + +#pragma warning disable xUnit2011 // Do not use empty collection check + Assert.Collection(list); +#pragma warning restore xUnit2011 // Do not use empty collection check + } + + [Fact] + public static void MismatchedElementCount() + { + var list = new List().ToAsyncEnumerable(); + + var ex = Record.Exception( +#pragma warning disable xUnit2023 // Do not use collection methods for single-item collections + () => Assert.Collection(list, + item => Assert.True(false) + ) +#pragma warning restore xUnit2023 // Do not use collection methods for single-item collections + ); + + var collEx = Assert.IsType(ex); + Assert.Equal( + "Assert.Collection() Failure: Mismatched item count" + Environment.NewLine + + "Collection: []" + Environment.NewLine + + "Expected count: 1" + Environment.NewLine + + "Actual count: 0", + collEx.Message + ); + Assert.Null(collEx.InnerException); + } + + [Fact] + public static void NonEmptyCollection() + { + var list = new List { 42, 2112 }.ToAsyncEnumerable(); + + Assert.Collection(list, + item => Assert.Equal(42, item), + item => Assert.Equal(2112, item) + ); + } + + [Fact] + public static void MismatchedElement() + { + var list = new List { 42, 2112 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => + Assert.Collection(list, + item => Assert.Equal(42, item), + item => Assert.Equal(2113, item) + ) + ); + + var collEx = Assert.IsType(ex); + Assert.StartsWith( + "Assert.Collection() Failure: Item comparison failure" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Collection: [42, 2112]" + Environment.NewLine + + "Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 2113" + Environment.NewLine + + " Actual: 2112" + Environment.NewLine + + " Stack Trace:", + ex.Message + ); + } + } + + public class CollectionAsync + { + [Fact] + public static async Task EmptyCollection() + { + var list = new List().ToAsyncEnumerable(); + +#pragma warning disable xUnit2011 // Do not use empty collection check + await Assert.CollectionAsync(list); +#pragma warning restore xUnit2011 // Do not use empty collection check + } + + [Fact] + public static async Task MismatchedElementCountAsync() + { + var list = new List().ToAsyncEnumerable(); + + var ex = await Record.ExceptionAsync( +#pragma warning disable xUnit2023 // Do not use collection methods for single-item collections + () => Assert.CollectionAsync(list, + async item => await Task.Yield() + ) +#pragma warning restore xUnit2023 // Do not use collection methods for single-item collections + ); + + var collEx = Assert.IsType(ex); + Assert.Equal( + "Assert.Collection() Failure: Mismatched item count" + Environment.NewLine + + "Collection: []" + Environment.NewLine + + "Expected count: 1" + Environment.NewLine + + "Actual count: 0", + collEx.Message + ); + Assert.Null(collEx.InnerException); + } + + [Fact] + public static async Task NonEmptyCollectionAsync() + { + var list = new List { 42, 2112 }.ToAsyncEnumerable(); + + await Assert.CollectionAsync(list, + async item => + { + await Task.Yield(); + Assert.Equal(42, item); + }, + async item => + { + await Task.Yield(); + Assert.Equal(2112, item); + } + ); + } + + [Fact] + public static async Task MismatchedElementAsync() + { + var list = new List { 42, 2112 }.ToAsyncEnumerable(); + + var ex = await Record.ExceptionAsync(() => + Assert.CollectionAsync(list, + async item => + { + await Task.Yield(); + Assert.Equal(42, item); + }, + async item => + { + await Task.Yield(); + Assert.Equal(2113, item); + } + ) + ); + + var collEx = Assert.IsType(ex); + Assert.StartsWith( + "Assert.Collection() Failure: Item comparison failure" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Collection: [42, 2112]" + Environment.NewLine + + "Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 2113" + Environment.NewLine + + " Actual: 2112" + Environment.NewLine + + " Stack Trace:", + ex.Message + ); + } + } + + public class Contains + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.Contains(14, default(IAsyncEnumerable)!)); + } + + [Fact] + public static void CanFindNullInContainer() + { + var list = new List { 16, null, "Hi there" }.ToAsyncEnumerable(); + + Assert.Contains(null, list); + } + + [Fact] + public static void ItemInContainer() + { + var list = new List { 42 }.ToAsyncEnumerable(); + + Assert.Contains(42, list); + } + + [Fact] + public static void ItemNotInContainer() + { + var list = new List { 41, 43 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Contains(42, list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Item not found in collection" + Environment.NewLine + + "Collection: [41, 43]" + Environment.NewLine + + "Not found: 42", + ex.Message + ); + } + + [Fact] + public static void NullsAllowedInContainer() + { + var list = new List { null, 16, "Hi there" }.ToAsyncEnumerable(); + + Assert.Contains("Hi there", list); + } + } + + public class Contains_Comparer + { + [Fact] + public static void GuardClauses() + { + var comparer = new MyComparer(); + + Assert.Throws("collection", () => Assert.Contains(14, default(IAsyncEnumerable)!, comparer)); + Assert.Throws("comparer", () => Assert.Contains(14, Array.Empty().ToAsyncEnumerable(), null!)); + } + + [Fact] + public static void CanUseComparer() + { + var list = new List { 42 }.ToAsyncEnumerable(); + + Assert.Contains(43, list, new MyComparer()); + } + + class MyComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + true; + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + } + + public class Contains_Predicate + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.Contains(default(IAsyncEnumerable)!, item => true)); + Assert.Throws("filter", () => Assert.Contains(Array.Empty().ToAsyncEnumerable(), (Predicate)null!)); + } + + [Fact] + public static void ItemFound() + { + var list = new[] { "Hello", "world" }.ToAsyncEnumerable(); + + Assert.Contains(list, item => item.StartsWith("wo")); + } + + [Fact] + public static void ItemNotFound() + { + var list = new[] { "Hello", "world" }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Contains(list, item => item.StartsWith("qu"))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Filter not matched in collection" + Environment.NewLine + + "Collection: [\"Hello\", \"world\"]", + ex.Message + ); + } + } + + public class Distinct + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.Distinct(default(IAsyncEnumerable)!)); + Assert.Throws("comparer", () => Assert.Distinct(Array.Empty().ToAsyncEnumerable(), null!)); + } + + [Fact] + public static void WithNull() + { + var list = new List { 16, "Hi there", null }.ToAsyncEnumerable(); + + Assert.Distinct(list); + } + + [Fact] + public static void TwoItems() + { + var list = new List { 42, 42 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Distinct(list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine + + "Collection: [42, 42]" + Environment.NewLine + + "Item: 42", + ex.Message + ); + } + + [Fact] + public static void TwoNulls() + { + var list = new List { "a", null, "b", null, "c", "d" }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Distinct(list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine + + $"Collection: [\"a\", null, \"b\", null, \"c\", {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Item: null", + ex.Message + ); + } + + [Fact] + public static void CaseSensitiveStrings() + { + var list = new string[] { "a", "b", "A" }.ToAsyncEnumerable(); + + Assert.Distinct(list); + Assert.Distinct(list, StringComparer.Ordinal); + } + + [Fact] + public static void CaseInsensitiveStrings() + { + var list = new string[] { "a", "b", "A" }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Distinct(list, StringComparer.OrdinalIgnoreCase)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine + + "Collection: [\"a\", \"b\", \"A\"]" + Environment.NewLine + + "Item: \"A\"", + ex.Message + ); + } + } + + public class DoesNotContain + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.DoesNotContain(14, default(IAsyncEnumerable)!)); + } + + [Fact] + public static void CanSearchForNullInContainer() + { + var list = new List { 16, "Hi there" }.ToAsyncEnumerable(); + + Assert.DoesNotContain(null, list); + } + + [Fact] + public static void ItemInContainer() + { + var list = new List { 42 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.DoesNotContain(42, list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Item found in collection" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Collection: [42]" + Environment.NewLine + + "Found: 42", + ex.Message + ); + } + + [Fact] + public static void ItemNotInContainer() + { + var list = new List().ToAsyncEnumerable(); + + Assert.DoesNotContain(42, list); + } + + [Fact] + public static void NullsAllowedInContainer() + { + var list = new List { null, 16, "Hi there" }.ToAsyncEnumerable(); + + Assert.DoesNotContain(42, list); + } + } + + public class DoesNotContain_Comparer + { + [Fact] + public static void GuardClauses() + { + var comparer = new MyComparer(); + + Assert.Throws("collection", () => Assert.DoesNotContain(14, default(IAsyncEnumerable)!, comparer)); + Assert.Throws("comparer", () => Assert.DoesNotContain(14, Array.Empty().ToAsyncEnumerable(), null!)); + } + + [Fact] + public static void CanUseComparer() + { + var list = new List { 42 }.ToAsyncEnumerable(); + + Assert.DoesNotContain(42, list, new MyComparer()); + } + + class MyComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + false; + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + } + + public class DoesNotContain_Predicate + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.DoesNotContain(default(IAsyncEnumerable)!, item => true)); + Assert.Throws("filter", () => Assert.DoesNotContain(Array.Empty().ToAsyncEnumerable(), (Predicate)null!)); + } + + [Fact] + public static void ItemFound() + { + var list = new[] { "Hello", "world" }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.DoesNotContain(list, item => item.StartsWith("wo"))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Filter matched in collection" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Collection: [\"Hello\", \"world\"]", + ex.Message + ); + } + + [Fact] + public static void ItemNotFound() + { + var list = new[] { "Hello", "world" }.ToAsyncEnumerable(); + + Assert.DoesNotContain(list, item => item.StartsWith("qu")); + } + } + + public class Empty + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.Empty(default(IAsyncEnumerable)!)); + } + + [Fact] + public static void EmptyCollection() + { + var list = new List().ToAsyncEnumerable(); + + Assert.Empty(list); + } + + [Fact] + public static void NonEmptyCollection() + { + var list = new List { 42 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Empty(list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Empty() Failure: Collection was not empty" + Environment.NewLine + + "Collection: [42]", + ex.Message + ); + } + + [Fact] + public static void CollectionEnumeratorDisposed() + { + var enumerator = new SpyEnumerator(Enumerable.Empty().ToAsyncEnumerable()); + + Assert.Empty(enumerator); + + Assert.True(enumerator.IsDisposed); + } + } + + public class Equal + { + public class Null + { + [Fact] + public static void BothNull() + { + var nullEnumerable = default(IEnumerable); + var nullAsyncEnumerable = default(IAsyncEnumerable); + + Assert.Equal(nullEnumerable, nullAsyncEnumerable); + Assert.Equal(nullAsyncEnumerable, nullAsyncEnumerable); + } + + [Fact] + public static void EmptyExpectedNullActual() + { + var expected = Array.Empty(); + var actual = default(IAsyncEnumerable); + + static void validateError( + Action action, + string expectedType) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: " + expectedType + " []" + Environment.NewLine + + "Actual: " + new string(' ', expectedType.Length) + " null", + ex.Message + ); + } + + validateError(() => Assert.Equal(expected, actual), "int[]"); + validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual), ""); + } + + [Fact] + public static void NullExpectedEmptyActual() + { + var actual = Array.Empty().ToAsyncEnumerable(); + + static void validateError(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: null" + Environment.NewLine + + "Actual: []", + ex.Message + ); + } + + validateError(() => Assert.Equal(default(IEnumerable), actual)); + validateError(() => Assert.Equal(default(IAsyncEnumerable), actual)); + } + } + + public class Collections + { + [Fact] + public static void Equal() + { + var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var actual = expected.ToAsyncEnumerable(); + + Assert.Equal(expected, actual); + Assert.Equal(expected.ToAsyncEnumerable(), actual); + } + + [Theory] + // Nulls + [InlineData( + null, null, "null", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "[1, 2, 3, 4, 5, $$ELLIPSIS$$]", null + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "[1, 2, 3, 4, 5, $$ELLIPSIS$$]", + null, "null", null + )] + // Start of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 0)", "[1, 2, 3, 4, 5, $$ELLIPSIS$$]", + new[] { 99, 2, 3, 4, 5, 6, 7 }, "[99, 2, 3, 4, 5, $$ELLIPSIS$$]", " ↑ (pos 0)" + )] + // Middle of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 3)", "[$$ELLIPSIS$$, 2, 3, 4, 5, 6, $$ELLIPSIS$$]", + new[] { 1, 2, 3, 99, 5, 6, 7 }, "[$$ELLIPSIS$$, 2, 3, 99, 5, 6, $$ELLIPSIS$$]", " ↑ (pos 3)" + )] + // End of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 6)", "[$$ELLIPSIS$$, 3, 4, 5, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 99 }, "[$$ELLIPSIS$$, 3, 4, 5, 6, 99]", " ↑ (pos 6)" + )] + //// Overruns + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "[$$ELLIPSIS$$, 4, 5, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, "[$$ELLIPSIS$$, 4, 5, 6, 7, 8]", " ↑ (pos 7)" + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "[$$ELLIPSIS$$, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, "[$$ELLIPSIS$$, 6, 7, 8, 9, 10, $$ELLIPSIS$$]", " ↑ (pos 7)" + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, " ↓ (pos 7)", "[$$ELLIPSIS$$, 4, 5, 6, 7, 8]", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "[$$ELLIPSIS$$, 4, 5, 6, 7]", null + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, " ↓ (pos 7)", "[$$ELLIPSIS$$, 6, 7, 8, 9, 10, $$ELLIPSIS$$]", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "[$$ELLIPSIS$$, 6, 7]", null + )] + public void NotEqual( + int[]? expected, + string? expectedPointer, + string expectedDisplay, + int[]? actualArray, + string actualDisplay, + string? actualPointer) + { + var actual = actualArray is null ? null : new List(actualArray).ToAsyncEnumerable(); + + void validateError( + Action action, + string expectedType) + { + var message = "Assert.Equal() Failure: Collections differ"; + var actualType = actualArray is null ? "" : " "; + + if (actualType == expectedType) + { + actualType = ""; + expectedType = ""; + } + + var padding = Math.Max(expectedType.Length, actualType.Length); + var paddingBlanks = new string(' ', padding); + + if (expectedPointer is not null) + message += Environment.NewLine + " " + paddingBlanks + expectedPointer; + + message += + Environment.NewLine + "Expected: " + expectedType.PadRight(padding) + expectedDisplay + + Environment.NewLine + "Actual: " + actualType.PadRight(padding) + actualDisplay; + + if (actualPointer is not null) + message += Environment.NewLine + " " + paddingBlanks + actualPointer; + + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal(message.Replace("$$ELLIPSIS$$", ArgumentFormatter.Ellipsis), ex.Message); + } + + validateError(() => Assert.Equal(expected, actual), expected is null ? "" : "int[] "); + validateError(() => Assert.Equal(expected?.ToAsyncEnumerable(), actual), expected is null ? "" : " "); + } + } + + public class CollectionsWithComparer + { + [Fact] + public static void AlwaysFalse() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = expected.ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine + + "Expected: " + expectedType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine + + " " + new string(' ', padding) + " ↑ (pos 0)", + ex.Message + ); + } + + validateError(() => Assert.Equal(expected, actual, new IntComparer(false)), "int[] ", " "); + validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual, new IntComparer(false)), "", ""); + } + + [Fact] + public static void AlwaysTrue() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new int[] { 0, 0, 0, 0, 0 }.ToAsyncEnumerable(); + + Assert.Equal(expected, actual, new IntComparer(true)); + Assert.Equal(expected.ToAsyncEnumerable(), actual, new IntComparer(true)); + } + + class IntComparer(bool answer) : + IEqualityComparer + { + public bool Equals(int x, int y) => answer; + + public int GetHashCode(int obj) => throw new NotImplementedException(); + } + + // https://github.com/xunit/xunit/issues/2795 + [Fact] + public void CollectionItemIsEnumerable() + { + List actual = [new(0), new(2)]; + List expected = [new(1), new(3)]; + + Assert.Equal(expected, actual.ToAsyncEnumerable(), new EnumerableItemComparer()); + Assert.Equal(expected.ToAsyncEnumerable(), actual.ToAsyncEnumerable(), new EnumerableItemComparer()); + } + + public class EnumerableItemComparer : IEqualityComparer + { + public bool Equals(EnumerableItem? x, EnumerableItem? y) => + x?.Value / 2 == y?.Value / 2; + + public int GetHashCode(EnumerableItem obj) => + throw new NotImplementedException(); + } + + public sealed class EnumerableItem(int value) : + IEnumerable + { + public int Value { get; } = value; + + public IEnumerator GetEnumerator() => + Enumerable.Repeat("", Value).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + } + + [Fact] + public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException() + { + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine + + " " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine + + "Expected: " + expectedType.PadRight(padding) + "[1, 2]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[1, 3]" + Environment.NewLine + + " " + new string(' ', padding) + " ↑ (pos 0)", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + +#pragma warning disable IDE0300 // Simplify collection initialization + validateError(() => Assert.Equal(new[] { 1, 2 }, new[] { 1, 3 }.ToAsyncEnumerable(), new ThrowingComparer()), "int[] ", " "); +#pragma warning restore IDE0300 // Simplify collection initialization + validateError(() => Assert.Equal(new[] { 1, 2 }.ToAsyncEnumerable(), new[] { 1, 3 }.ToAsyncEnumerable(), new ThrowingComparer()), "", ""); + } + + public class ThrowingComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + throw new DivideByZeroException(); + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + } + + public class CollectionsWithEquatable + { + [Fact] + public void Equal() + { + var expected = new[] { new EquatableObject { Char = 'a' } }; + var actual = new[] { new EquatableObject { Char = 'a' } }.ToAsyncEnumerable(); + + Assert.Equal(expected, actual); + Assert.Equal(expected.ToAsyncEnumerable(), actual); + } + + [Fact] + public void NotEqual() + { + var expected = new[] { new EquatableObject { Char = 'a' } }; + var actual = new[] { new EquatableObject { Char = 'b' } }.ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine + +#if XUNIT_AOT + "Expected: " + expectedType.PadRight(padding) + $"[EquatableObject {{ {ArgumentFormatter.Ellipsis} }}]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + $"[EquatableObject {{ {ArgumentFormatter.Ellipsis} }}]" + Environment.NewLine + +#else + "Expected: " + expectedType.PadRight(padding) + "[EquatableObject { Char = 'a' }]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[EquatableObject { Char = 'b' }]" + Environment.NewLine + +#endif + " " + new string(' ', padding) + " ↑ (pos 0)", + ex.Message + ); + } + + validateError(() => Assert.Equal(expected, actual), "EquatableObject[] ", " "); + validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual), "", ""); + } + + public class EquatableObject : IEquatable + { + public char Char { get; set; } + + public bool Equals(EquatableObject? other) => + other != null && other.Char == Char; + + public override bool Equals(object? obj) => + Equals(obj as EquatableObject); + + public override int GetHashCode() => + Char.GetHashCode(); + } + } + + public class CollectionsWithFunc + { + [Fact] + public static void AlwaysFalse() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List([1, 2, 3, 4, 5]).ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine + + "Expected: " + expectedType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine + + " " + new string(' ', padding) + " ↑ (pos 0)", + ex.Message + ); + } + + validateError(() => Assert.Equal(expected, actual, (x, y) => false), "int[] ", " "); + validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual, (int x, int y) => false), "", ""); + } + + [Fact] + public static void AlwaysTrue() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List([0, 0, 0, 0, 0]).ToAsyncEnumerable(); + + Assert.Equal(expected, actual, (x, y) => true); + Assert.Equal(expected.ToAsyncEnumerable(), actual, (int x, int y) => true); + } + + // https://github.com/xunit/xunit/issues/2795 + [Fact] + public void CollectionItemIsEnumerable() + { + var expected = new List { new(1), new(3) }; + var actual = new List { new(0), new(2) }.ToAsyncEnumerable(); + + Assert.Equal(expected, actual, (x, y) => x.Value / 2 == y.Value / 2); + Assert.Equal(expected.ToAsyncEnumerable(), actual, (x, y) => x.Value / 2 == y.Value / 2); + } + + public sealed class EnumerableItem(int value) : + IEnumerable + { + public int Value { get; } = value; + + public IEnumerator GetEnumerator() => + Enumerable.Repeat("", Value).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + } + + [Fact] + public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException() + { + var expected = new[] { 1, 2 }; + var actual = new[] { 1, 3 }.ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine + + " " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine + + "Expected: " + expectedType.PadRight(padding) + "[1, 2]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[1, 3]" + Environment.NewLine + + " " + new string(' ', padding) + " ↑ (pos 0)", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + validateError(() => Assert.Equal(expected, actual, (e, a) => throw new DivideByZeroException()), "int[] ", " "); + validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual, (int e, int a) => throw new DivideByZeroException()), "", ""); + } + } + } + + public class NotEmpty + { + [Fact] + public static void EmptyContainer() + { + var list = new List().ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.NotEmpty(list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEmpty() Failure: Collection was empty", + ex.Message + ); + } + + [Fact] + public static void NonEmptyContainer() + { + var list = new List { 42 }.ToAsyncEnumerable(); + + Assert.NotEmpty(list); + } + + [Fact] + public static void EnumeratorDisposed() + { + var enumerator = new SpyEnumerator(Enumerable.Range(0, 1).ToAsyncEnumerable()); + + Assert.NotEmpty(enumerator); + + Assert.True(enumerator.IsDisposed); + } + } + + public class NotEqual + { + public class Null + { + [Fact] + public static void BothNull() + { + var nullEnumerable = default(IEnumerable); + var nullAsyncEnumerable = default(IAsyncEnumerable); + + static void validateError(Action action) + { + var ex = Record.Exception(action); + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not null" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + + validateError(() => Assert.NotEqual(nullEnumerable, nullAsyncEnumerable)); + validateError(() => Assert.NotEqual(nullAsyncEnumerable, nullAsyncEnumerable)); + } + + [Fact] + public static void EmptyExpectedNullActual() + { + var expected = Array.Empty(); + var actual = default(IAsyncEnumerable); + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected.ToAsyncEnumerable(), actual); + } + + [Fact] + public static void NullExpectedEmptyActual() + { + var actual = Array.Empty().ToAsyncEnumerable(); + + Assert.NotEqual(default(IEnumerable), actual); + Assert.NotEqual(default(IAsyncEnumerable), actual); + } + } + + public class Collections + { + [Fact] + public static void Equal() + { + var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var actual = new List(expected).ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + $"Expected: Not {expectedType.PadRight(padding)}[1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + $"Actual: {actualType.PadRight(padding)}[1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]", + ex.Message + ); + } + + validateError(() => Assert.NotEqual(expected, actual), "int[] ", " "); + validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual), "", ""); + } + + [Fact] + public static void NotEqual() + { + var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var actual = new List([1, 2, 3, 4, 0, 6, 7, 8, 9, 10]).ToAsyncEnumerable(); + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected.ToAsyncEnumerable(), actual); + } + } + + public class CollectionsWithComparer + { + [Fact] + public static void AlwaysFalse() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List([1, 2, 3, 4, 5]).ToAsyncEnumerable(); + + Assert.NotEqual(expected, actual, new IntComparer(false)); + Assert.NotEqual(expected.ToAsyncEnumerable(), actual, new IntComparer(false)); + } + + [Fact] + public static void AlwaysTrue() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List([0, 0, 0, 0, 0]).ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not " + expectedType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[0, 0, 0, 0, 0]", + ex.Message + ); + } + + validateError(() => Assert.NotEqual(expected, actual, new IntComparer(true)), "int[] ", " "); + validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual, new IntComparer(true)), "", ""); + } + + class IntComparer(bool answer) : + IEqualityComparer + { + public bool Equals(int x, int y) => + answer; + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + + [Fact] + public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException() + { + var expected = new[] { 1, 2 }; + var actual = new[] { 1, 2 }.ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + " " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine + + "Expected: Not " + expectedType.PadRight(padding) + "[1, 2]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[1, 2]" + Environment.NewLine + + " " + new string(' ', padding) + " ↑ (pos 0)", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + validateError(() => Assert.NotEqual(expected, actual, new ThrowingComparer()), "int[] ", " "); + validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual, new ThrowingComparer()), "", ""); + } + + public class ThrowingComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + throw new DivideByZeroException(); + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + } + + public class CollectionsWithEquatable + { + [Fact] + public void Equal() + { + var expected = new[] { new EquatableObject { Char = 'a' } }; + var actual = new[] { new EquatableObject { Char = 'a' } }.ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not " + expectedType.PadRight(padding) + $"[EquatableObject {{ {ArgumentFormatter.Ellipsis} }}]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + $"[EquatableObject {{ {ArgumentFormatter.Ellipsis} }}]", +#else + "Expected: Not " + expectedType.PadRight(padding) + "[EquatableObject { Char = 'a' }]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[EquatableObject { Char = 'a' }]", +#endif + ex.Message + ); + } + + validateError(() => Assert.NotEqual(expected, actual), "EquatableObject[] ", " "); + validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual), "", ""); + } + + [Fact] + public void NotEqual() + { + var expected = new[] { new EquatableObject { Char = 'a' } }; + var actual = new[] { new EquatableObject { Char = 'b' } }.ToAsyncEnumerable(); + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected.ToAsyncEnumerable(), actual); + } + + public class EquatableObject : IEquatable + { + public char Char { get; set; } + + public bool Equals(EquatableObject? other) => + other != null && other.Char == Char; + + public override bool Equals(object? obj) => + Equals(obj as EquatableObject); + + public override int GetHashCode() => + Char.GetHashCode(); + } + } + + public class CollectionsWithFunc + { + [Fact] + public static void AlwaysFalse() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List([1, 2, 3, 4, 5]).ToAsyncEnumerable(); + + Assert.NotEqual(expected, actual, (x, y) => false); + Assert.NotEqual(expected.ToAsyncEnumerable(), actual, (int x, int y) => false); + } + + [Fact] + public static void AlwaysTrue() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List([0, 0, 0, 0, 0]).ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not " + expectedType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[0, 0, 0, 0, 0]", + ex.Message + ); + } + + validateError(() => Assert.NotEqual(expected, actual, (x, y) => true), "int[] ", " "); + validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual, (int x, int y) => true), "", ""); + } + + [Fact] + public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException() + { + var expected = new[] { 1, 2 }; + var actual = new[] { 1, 2 }.ToAsyncEnumerable(); + + static void validateError( + Action action, + string expectedType, + string actualType) + { + var ex = Record.Exception(action); + var padding = Math.Max(expectedType.Length, actualType.Length); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + " " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine + + "Expected: Not " + expectedType.PadRight(padding) + "[1, 2]" + Environment.NewLine + + "Actual: " + actualType.PadRight(padding) + "[1, 2]" + Environment.NewLine + + " " + new string(' ', padding) + " ↑ (pos 0)", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + validateError(() => Assert.NotEqual(expected, actual, (e, a) => throw new DivideByZeroException()), "int[] ", " "); + validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual, (int e, int a) => throw new DivideByZeroException()), "", ""); + } + } + } + + public class Single + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.Single(default(IAsyncEnumerable)!)); + } + + [Fact] + public static void EmptyCollection() + { + var collection = Array.Empty().ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal("Assert.Single() Failure: The collection was empty", ex.Message); + } + + [Fact] + public static void SingleItemCollection() + { + var collection = new[] { "Hello" }.ToAsyncEnumerable(); + + var item = Assert.Single(collection); + + Assert.Equal("Hello", item); + } + + [Fact] + public static void MultiItemCollection() + { + var collection = new[] { "Hello", "World" }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 items" + Environment.NewLine + + "Collection: [\"Hello\", \"World\"]", + ex.Message + ); + } + + [Fact] + public static void Truncation() + { + var collection = new[] { 1, 2, 3, 4, 5, 6, 7 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 7 items" + Environment.NewLine + + $"Collection: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]", + ex.Message + ); + } + + [Fact] + public static void StringAsCollection_Match() + { + var collection = "H".ToAsyncEnumerable(); + + var value = Assert.Single(collection); + + Assert.Equal('H', value); + } + + [Fact] + public static void StringAsCollection_NoMatch() + { + var collection = "Hello".ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 5 items" + Environment.NewLine + + "Collection: ['H', 'e', 'l', 'l', 'o']", + ex.Message + ); + } + } + + public class Single_WithPredicate + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.Single(default(IAsyncEnumerable)!, _ => true)); + Assert.Throws("predicate", () => Assert.Single(Array.Empty().ToAsyncEnumerable(), null!)); + } + + [Fact] + public static void SingleMatch() + { + var collection = new[] { "Hello", "World" }.ToAsyncEnumerable(); + + var result = Assert.Single(collection, static item => item.StartsWith('H')); + + Assert.Equal("Hello", result); + } + + [Fact] + public static void NoMatches() + { + var collection = new[] { "Hello", "World" }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Single(collection, item => false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection did not contain any matching items" + Environment.NewLine + + "Expected: (predicate expression)" + Environment.NewLine + + "Collection: [\"Hello\", \"World\"]", + ex.Message + ); + } + + [Fact] + public static void TooManyMatches() + { + var collection = new[] { "Hello", "World" }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Single(collection, item => true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 matching items" + Environment.NewLine + + "Expected: (predicate expression)" + Environment.NewLine + + "Collection: [\"Hello\", \"World\"]" + Environment.NewLine + + "Match indices: 0, 1", + ex.Message + ); + } + + [Fact] + public static void Truncation() + { + var collection = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 4 }.ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Single(collection, item => item == 4)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 matching items" + Environment.NewLine + + "Expected: (predicate expression)" + Environment.NewLine + + $"Collection: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Match indices: 3, 8", + ex.Message + ); + } + + [Fact] + public static void StringAsCollection_Match() + { + var collection = "H".ToAsyncEnumerable(); + + var value = Assert.Single(collection, c => c != 'Q'); + + Assert.Equal('H', value); + } + + [Fact] + public static void StringAsCollection_NoMatch() + { + var collection = "H".ToAsyncEnumerable(); + + var ex = Record.Exception(() => Assert.Single(collection, c => c == 'Q')); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection did not contain any matching items" + Environment.NewLine + + "Expected: (predicate expression)" + Environment.NewLine + + "Collection: ['H']", + ex.Message + ); + } + } + + sealed class SpyEnumerator(IAsyncEnumerable enumerable) : + IAsyncEnumerable, IAsyncEnumerator + { + IAsyncEnumerator? innerEnumerator = enumerable.GetAsyncEnumerator(); + + public T Current => + GuardNotNull("Tried to get Current on a disposed enumerator", innerEnumerator).Current; + + public bool IsDisposed => + innerEnumerator is null; + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => this; + + public ValueTask MoveNextAsync() => + GuardNotNull("Tried to call MoveNext() on a disposed enumerator", innerEnumerator).MoveNextAsync(); + + public async ValueTask DisposeAsync() + { + if (innerEnumerator is not null) + await innerEnumerator.DisposeAsync(); + + innerEnumerator = null; + } + + /// + static T2 GuardNotNull( + string message, + [NotNull] T2? value) + where T2 : class + { + if (value is null) + throw new InvalidOperationException(message); + + return value; + } + } +} + +#endif diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/BooleanAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/BooleanAssertsTests.cs new file mode 100644 index 00000000000..ce6ed22d921 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/BooleanAssertsTests.cs @@ -0,0 +1,101 @@ +using Xunit; +using Xunit.Sdk; + +public class BooleanAssertsTests +{ + public class False + { + [Fact] + public static void AssertFalse() + { + Assert.False(false); + } + + [Fact] + public static void ThrowsExceptionWhenTrue() + { + var ex = Record.Exception(() => Assert.False(true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.False() Failure" + Environment.NewLine + + "Expected: False" + Environment.NewLine + + "Actual: True", + ex.Message + ); + } + + [Fact] + public static void ThrowsExceptionWhenNull() + { + var ex = Record.Exception(() => Assert.False(null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.False() Failure" + Environment.NewLine + + "Expected: False" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + + [Fact] + public static void UserSuppliedMessage() + { +#pragma warning disable xUnit2020 // Do not use always-failing boolean assertions + var ex = Record.Exception(() => Assert.False(true, "Custom User Message")); +#pragma warning restore xUnit2020 // Do not use always-failing boolean assertions + + Assert.NotNull(ex); + Assert.Equal("Custom User Message", ex.Message); + } + } + + public class True + { + [Fact] + public static void AssertTrue() + { + Assert.True(true); + } + + [Fact] + public static void ThrowsExceptionWhenFalse() + { + var ex = Record.Exception(() => Assert.True(false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.True() Failure" + Environment.NewLine + + "Expected: True" + Environment.NewLine + + "Actual: False", + ex.Message + ); + } + + [Fact] + public static void ThrowsExceptionWhenNull() + { + var ex = Record.Exception(() => Assert.True(null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.True() Failure" + Environment.NewLine + + "Expected: True" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + + [Fact] + public static void UserSuppliedMessage() + { +#pragma warning disable xUnit2020 // Do not use always-failing boolean assertions + var ex = Record.Exception(() => Assert.True(false, "Custom User Message")); +#pragma warning restore xUnit2020 // Do not use always-failing boolean assertions + + Assert.NotNull(ex); + Assert.Equal("Custom User Message", ex.Message); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/CollectionAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/CollectionAssertsTests.cs new file mode 100644 index 00000000000..6167acf6c43 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/CollectionAssertsTests.cs @@ -0,0 +1,2755 @@ +#pragma warning disable CA1825 // Avoid zero-length array allocations +#pragma warning disable CA1865 // Use char overload +#pragma warning disable IDE0034 // Simplify 'default' expression + +using System.Collections; +using System.Text; +using Xunit; +using Xunit.Sdk; + +public class CollectionAssertsTests +{ + public class All + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.All(default(IEnumerable)!, _ => { })); + Assert.Throws("action", () => Assert.All([], (Action)null!)); + Assert.Throws("action", () => Assert.All([], (Action)null!)); + } + + [Fact] + public static void Success() + { + var items = new[] { 1, 1, 1, 1, 1, 1 }; + + Assert.All(items, x => Assert.Equal(1, x)); + } + + [Fact] + public static void EmptyCollection_ThrowIfEmptyFalse_Success() + { + var items = Array.Empty(); + Assert.All(items, item => Assert.Equal(1, item), false); + } + + [Fact] + public static void Failure() + { + var items = new[] { 1, 1, 42, 2112, 1, 1 }; + + var ex = Record.Exception(() => Assert.All(items, item => Assert.Equal(1, item))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.All() Failure: 2 out of 6 items in the collection did not pass." + Environment.NewLine + + "[2]: Item: 42" + Environment.NewLine + + " Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 1" + Environment.NewLine + + " Actual: 42" + Environment.NewLine + + "[3]: Item: 2112" + Environment.NewLine + + " Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 1" + Environment.NewLine + + " Actual: 2112", + ex.Message + ); + } + + [Fact] + public static void EmptyCollection_ThrowIfEmptyTrue_Failure() + { + var items = Array.Empty(); + var ex = Record.Exception(() => Assert.All(items, item => Assert.Equal(1, item), true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.All() Failure: The collection was empty." + Environment.NewLine + + "At least one item was expected.", + ex.Message + ); + } + + [Fact] + public static void ActionCanReceiveIndex() + { + var items = new[] { 1, 1, 2, 2, 1, 1 }; + var indices = new List(); + + Assert.All(items, (_, idx) => indices.Add(idx)); + + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, indices); + } + } + + public class AllAsync + { + [Fact] + public static async Task GuardClauses() + { + await Assert.ThrowsAsync("collection", () => Assert.AllAsync(default(IEnumerable)!, async _ => await Task.Yield())); + await Assert.ThrowsAsync("action", () => Assert.AllAsync([], (Func)null!)); + await Assert.ThrowsAsync("action", () => Assert.AllAsync([], (Func)null!)); + } + + [Fact] + public static async Task Success() + { + var items = new[] { 1, 1, 1, 1, 1, 1 }; + + await Assert.AllAsync(items, async item => { await Task.Yield(); Assert.Equal(1, item); }); + } + + [Fact] + public static async Task EmptyCollection_ThrowIfEmptyFalse_Success() + { + var items = Array.Empty(); + await Assert.AllAsync(items, async item => { await Task.Yield(); Assert.Equal(1, item); }, false); + } + + [Fact] + public static void Failure() + { + var items = new[] { 1, 1, 42, 2112, 1, 1 }; + + var ex = Record.Exception(() => Assert.All(items, x => Assert.Equal(1, x))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.All() Failure: 2 out of 6 items in the collection did not pass." + Environment.NewLine + + "[2]: Item: 42" + Environment.NewLine + + " Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 1" + Environment.NewLine + + " Actual: 42" + Environment.NewLine + + "[3]: Item: 2112" + Environment.NewLine + + " Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 1" + Environment.NewLine + + " Actual: 2112", + ex.Message + ); + } + + [Fact] + public static async Task EmptyCollection_ThrowIfEmptyTrue_Failure() + { + var items = Array.Empty(); + var ex = await Record.ExceptionAsync(() => Assert.AllAsync(items, async item => { await Task.Yield(); Assert.Equal(1, item); }, true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.All() Failure: The collection was empty." + Environment.NewLine + + "At least one item was expected.", + ex.Message + ); + } + + [Fact] + public static async Task ActionCanReceiveIndex() + { + var items = new[] { 1, 1, 2, 2, 1, 1 }; + var indices = new List(); + + await Assert.AllAsync(items, async (_, idx) => { await Task.Yield(); indices.Add(idx); }); + + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, indices); + } + } + + public class Collection + { + [Fact] + public static void EmptyCollection() + { + var list = new List(); + +#pragma warning disable xUnit2011 // Do not use empty collection check + Assert.Collection(list); +#pragma warning restore xUnit2011 // Do not use empty collection check + } + + [Fact] + public static void MismatchedElementCount() + { + var list = new List(); + + var ex = Record.Exception( +#pragma warning disable xUnit2023 // Do not use collection methods for single-item collections + () => Assert.Collection(list, + item => Assert.True(false) + ) +#pragma warning restore xUnit2023 // Do not use collection methods for single-item collections + ); + + var collEx = Assert.IsType(ex); + Assert.Equal( + "Assert.Collection() Failure: Mismatched item count" + Environment.NewLine + + "Collection: []" + Environment.NewLine + + "Expected count: 1" + Environment.NewLine + + "Actual count: 0", + collEx.Message + ); + Assert.Null(collEx.InnerException); + } + + [Fact] + public static void NonEmptyCollection() + { + var list = new List { 42, 2112 }; + + Assert.Collection(list, + item => Assert.Equal(42, item), + item => Assert.Equal(2112, item) + ); + } + + [Fact] + public static void MismatchedElement() + { + var list = new List { 42, 2112 }; + + var ex = Record.Exception(() => + Assert.Collection(list, + item => Assert.Equal(42, item), + item => Assert.Equal(2113, item) + ) + ); + + var collEx = Assert.IsType(ex); + Assert.StartsWith( + "Assert.Collection() Failure: Item comparison failure" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Collection: [42, 2112]" + Environment.NewLine + + "Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 2113" + Environment.NewLine + + " Actual: 2112" + Environment.NewLine + + " Stack Trace:", + ex.Message + ); + } + } + + public class CollectionAsync + { + [Fact] + public static async Task EmptyCollection() + { + var list = new List(); + +#pragma warning disable xUnit2011 // Do not use empty collection check + await Assert.CollectionAsync(list); +#pragma warning restore xUnit2011 // Do not use empty collection check + } + + [Fact] + public static async Task MismatchedElementCountAsync() + { + var list = new List(); + + var ex = await Record.ExceptionAsync( +#pragma warning disable xUnit2023 // Do not use collection methods for single-item collections + () => Assert.CollectionAsync(list, + async item => await Task.Yield() + ) +#pragma warning restore xUnit2023 // Do not use collection methods for single-item collections + ); + + var collEx = Assert.IsType(ex); + Assert.Equal( + "Assert.Collection() Failure: Mismatched item count" + Environment.NewLine + + "Collection: []" + Environment.NewLine + + "Expected count: 1" + Environment.NewLine + + "Actual count: 0", + collEx.Message + ); + Assert.Null(collEx.InnerException); + } + + [Fact] + public static async Task NonEmptyCollectionAsync() + { + var list = new List { 42, 2112 }; + + await Assert.CollectionAsync(list, + async item => + { + await Task.Yield(); + Assert.Equal(42, item); + }, + async item => + { + await Task.Yield(); + Assert.Equal(2112, item); + } + ); + } + + [Fact] + public static async Task MismatchedElementAsync() + { + var list = new List { 42, 2112 }; + + var ex = await Record.ExceptionAsync(() => + Assert.CollectionAsync(list, + async item => + { + await Task.Yield(); + Assert.Equal(42, item); + }, + async item => + { + await Task.Yield(); + Assert.Equal(2113, item); + } + ) + ); + + var collEx = Assert.IsType(ex); + Assert.StartsWith( + "Assert.Collection() Failure: Item comparison failure" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Collection: [42, 2112]" + Environment.NewLine + + "Error: Assert.Equal() Failure: Values differ" + Environment.NewLine + + " Expected: 2113" + Environment.NewLine + + " Actual: 2112" + Environment.NewLine + + " Stack Trace:", + ex.Message + ); + } + } + + public class Contains + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.Contains(14, default(IEnumerable)!)); + } + + [Fact] + public static void CanFindNullInContainer() + { + var list = new List { 16, null, "Hi there" }; + + Assert.Contains(null, list); + } + + [Fact] + public static void ItemInContainer() + { + var list = new List { 42 }; + + Assert.Contains(42, list); + } + + [Fact] + public static void ItemNotInContainer() + { + var list = new List { 41, 43 }; + + var ex = Record.Exception(() => Assert.Contains(42, list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Item not found in collection" + Environment.NewLine + + "Collection: [41, 43]" + Environment.NewLine + + "Not found: 42", + ex.Message + ); + } + + [Fact] + public static void NullsAllowedInContainer() + { + var list = new List { null, 16, "Hi there" }; + + Assert.Contains("Hi there", list); + } + + [Fact] + public static void SetsAreTreatedSpecially() + { + IEnumerable set = new HashSet(StringComparer.OrdinalIgnoreCase) { "Hi there" }; + + Assert.Contains("HI THERE", set); + } + +#if NET8_0_OR_GREATER + + [Fact] + public static void ReadOnlySetsAreTreatedSpecially() + { + IEnumerable set = new ReadOnlySet(StringComparer.OrdinalIgnoreCase, "Hi there"); + + Assert.Contains("HI THERE", set); + } + +#endif // NET8_0_OR_GREATER + } + + public class Contains_Comparer + { + [Fact] + public static void GuardClauses() + { + var comparer = new MyComparer(); + + Assert.Throws("collection", () => Assert.Contains(14, default(IEnumerable)!, comparer)); + Assert.Throws("comparer", () => Assert.Contains(14, [], null!)); + } + + [Fact] + public static void CanUseComparer() + { + var list = new List { 42 }; + + Assert.Contains(43, list, new MyComparer()); + } + + [Fact] + public static void HashSetConstructorComparerIsIgnored() + { + IEnumerable set = new HashSet(StringComparer.OrdinalIgnoreCase) { "Hi there" }; + + var ex = Record.Exception(() => Assert.Contains("HI THERE", set, StringComparer.Ordinal)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Item not found in collection" + Environment.NewLine + + "Collection: [\"Hi there\"]" + Environment.NewLine + + "Not found: \"HI THERE\"", + ex.Message + ); + } + + class MyComparer : IEqualityComparer + { + public bool Equals(int x, int y) => true; + + public int GetHashCode(int obj) => throw new NotImplementedException(); + } + } + + public class Contains_Predicate + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.Contains(default(IEnumerable)!, item => true)); + Assert.Throws("filter", () => Assert.Contains([], (Predicate)null!)); + } + + [Fact] + public static void ItemFound() + { + var list = new[] { "Hello", "world" }; + + Assert.Contains(list, item => item.StartsWith("w", StringComparison.InvariantCulture)); + } + + [Fact] + public static void ItemNotFound() + { + var list = new[] { "Hello", "world" }; + + var ex = Record.Exception(() => Assert.Contains(list, item => item.StartsWith("q", StringComparison.InvariantCulture))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Filter not matched in collection" + Environment.NewLine + + "Collection: [\"Hello\", \"world\"]", + ex.Message + ); + } + } + + public class Distinct + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.Distinct(default(IEnumerable)!)); + Assert.Throws("comparer", () => Assert.Distinct(Array.Empty(), null!)); + } + + [Fact] + public static void WithNull() + { + var list = new List { 16, "Hi there", null }; + + Assert.Distinct(list); + } + + [Fact] + public static void TwoItems() + { + var list = new List { 42, 42 }; + + var ex = Record.Exception(() => Assert.Distinct(list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine + + "Collection: [42, 42]" + Environment.NewLine + + "Item: 42", + ex.Message + ); + } + + [Fact] + public static void TwoNulls() + { + var list = new List { "a", null, "b", null, "c", "d" }; + + var ex = Record.Exception(() => Assert.Distinct(list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine + + $"Collection: [\"a\", null, \"b\", null, \"c\", {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Item: null", + ex.Message + ); + } + + [Fact] + public static void CaseSensitiveStrings() + { + var list = new string[] { "a", "b", "A" }; + + Assert.Distinct(list); + Assert.Distinct(list, StringComparer.Ordinal); + } + + [Fact] + public static void CaseInsensitiveStrings() + { + var list = new string[] { "a", "b", "A" }; + + var ex = Record.Exception(() => Assert.Distinct(list, StringComparer.OrdinalIgnoreCase)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine + + "Collection: [\"a\", \"b\", \"A\"]" + Environment.NewLine + + "Item: \"A\"", + ex.Message + ); + } + } + + public class DoesNotContain + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.DoesNotContain(14, default(IEnumerable)!)); + } + + [Fact] + public static void CanSearchForNullInContainer() + { + var list = new List { 16, "Hi there" }; + + Assert.DoesNotContain(null, list); + } + + [Fact] + public static void ItemInContainer() + { + var list = new List { 42 }; + + var ex = Record.Exception(() => Assert.DoesNotContain(42, list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Item found in collection" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Collection: [42]" + Environment.NewLine + + "Found: 42", + ex.Message + ); + } + + [Fact] + public static void ItemNotInContainer() + { + var list = new List(); + + Assert.DoesNotContain(42, list); + } + + [Fact] + public static void NullsAllowedInContainer() + { + var list = new List { null, 16, "Hi there" }; + + Assert.DoesNotContain(42, list); + } + + [Fact] + public static void SetsAreTreatedSpecially() + { + IEnumerable set = new HashSet(StringComparer.OrdinalIgnoreCase) { "Hi there" }; + + var ex = Record.Exception(() => Assert.DoesNotContain("HI THERE", set)); + + Assert.IsType(ex); + // Note: There is no pointer for sets, unlike other collections + Assert.Equal( + "Assert.DoesNotContain() Failure: Item found in set" + Environment.NewLine + + "Set: [\"Hi there\"]" + Environment.NewLine + + "Found: \"HI THERE\"", + ex.Message + ); + } + +#if NET8_0_OR_GREATER + + [Fact] + public static void ReadOnlySetsAreTreatedSpecially() + { + IEnumerable set = new ReadOnlySet(StringComparer.OrdinalIgnoreCase, "Hi there"); + + var ex = Record.Exception(() => Assert.DoesNotContain("HI THERE", set)); + + Assert.IsType(ex); + // Note: There is no pointer for sets, unlike other collections + Assert.Equal( + "Assert.DoesNotContain() Failure: Item found in set" + Environment.NewLine + + "Set: [\"Hi there\"]" + Environment.NewLine + + "Found: \"HI THERE\"", + ex.Message + ); + } + +#endif // NET8_0_OR_GREATER + } + + public class DoesNotContain_Comparer + { + [Fact] + public static void GuardClauses() + { + var comparer = new MyComparer(); + + Assert.Throws("collection", () => Assert.DoesNotContain(14, default(IEnumerable)!, comparer)); + Assert.Throws("comparer", () => Assert.DoesNotContain(14, [], null!)); + } + + [Fact] + public static void CanUseComparer() + { + var list = new List { 42 }; + + Assert.DoesNotContain(42, list, new MyComparer()); + } + + [Fact] + public static void HashSetConstructorComparerIsIgnored() + { + IEnumerable set = new HashSet(StringComparer.OrdinalIgnoreCase) { "Hi there" }; + + Assert.DoesNotContain("HI THERE", set, StringComparer.Ordinal); + } + + class MyComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + false; + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + } + + public class DoesNotContain_Predicate + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.DoesNotContain(default(IEnumerable)!, item => true)); + Assert.Throws("filter", () => Assert.DoesNotContain([], (Predicate)null!)); + } + + [Fact] + public static void ItemFound() + { + var list = new[] { "Hello", "world" }; + + var ex = Record.Exception(() => Assert.DoesNotContain(list, item => item.StartsWith("w", StringComparison.InvariantCulture))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Filter matched in collection" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Collection: [\"Hello\", \"world\"]", + ex.Message + ); + } + + [Fact] + public static void ItemNotFound() + { + var list = new[] { "Hello", "world" }; + + Assert.DoesNotContain(list, item => item.StartsWith("q", StringComparison.InvariantCulture)); + } + } + + public class Empty + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.Empty(default(IEnumerable)!)); + } + + [Fact] + public static void EmptyCollection() + { + var list = new List(); + + Assert.Empty(list); + } + + [Fact] + public static void NonEmptyCollection() + { + var list = new List { 42 }; + + var ex = Record.Exception(() => Assert.Empty(list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Empty() Failure: Collection was not empty" + Environment.NewLine + + "Collection: [42]", + ex.Message + ); + } + + [Fact] + public static void CollectionEnumeratorDisposed() + { + var enumerator = new SpyEnumerator([]); + + Assert.Empty(enumerator); + + Assert.True(enumerator.IsDisposed); + } + } + + public class Equal + { + public class Null + { + [Fact] + public static void BothNull() + { + var expected = default(IEnumerable); + var actual = default(IEnumerable); + + Assert.Equal(expected, actual); + } + + [Fact] + public static void EmptyExpectedNullActual() + { + var expected = new int[0]; + var actual = default(IEnumerable); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: int[] []" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + + [Fact] + public static void NullExpectedEmptyActual() + { + var expected = default(IEnumerable); + var actual = new int[0]; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: null" + Environment.NewLine + + "Actual: int[] []", + ex.Message + ); + } + } + + public class Arrays + { + [Fact] + public static void Equal() + { + string[] expected = ["@", "a", "ab", "b"]; + string[] actual = ["@", "a", "ab", "b"]; + + Assert.Equal(expected, actual); + } + + [Fact] + public static void EmbeddedArrays_Equal() + { + string[][] expected = [["@", "a"], ["ab", "b"]]; + string[][] actual = [["@", "a"], ["ab", "b"]]; + + Assert.Equal(expected, actual); + } + + [Theory] + // Nulls + [InlineData(null, new[] { 1, 2, 3 }, null, null)] + [InlineData(new[] { 1, 2, 3 }, null, null, null)] + // Difference at start + [InlineData(new[] { 0, 2, 3, 4 }, new[] { 1, 2, 3, 4 }, "↓ (pos 0)", "↑ (pos 0)")] + // Inline difference + [InlineData(new[] { 1, 0, 3, 4 }, new[] { 1, 2, 3, 4 }, " ↓ (pos 1)", " ↑ (pos 1)")] + // Difference at end + [InlineData(new[] { 1, 2, 3, 0 }, new[] { 1, 2, 3, 4 }, " ↓ (pos 3)", " ↑ (pos 3)")] + // Overruns + [InlineData(new[] { 1, 2, 3, 4 }, new[] { 1, 2, 3, 4, 5 }, null, " ↑ (pos 4)")] + [InlineData(new[] { 1, 2, 3, 4, 5 }, new[] { 1, 2, 3, 4 }, " ↓ (pos 4)", null)] + [InlineData(new[] { 1 }, new int[0], "↓ (pos 0)", null)] + [InlineData(new int[0], new[] { 1 }, null, "↑ (pos 0)")] + public void NotEqual( + int[]? expected, + int[]? actual, + string? expectedPointer, + string? actualPointer) + { + string message = "Assert.Equal() Failure: Collections differ"; + + if (expectedPointer is not null) + message += Environment.NewLine + " " + expectedPointer; + + var (expectedType, actualType) = (expected, actual) switch + { + (null, _) => (" ", "int[] "), + (_, null) => ("int[] ", " "), + (_, _) => ("", ""), + }; + + message += + Environment.NewLine + "Expected: " + expectedType + ArgumentFormatter.Format(expected) + + Environment.NewLine + "Actual: " + actualType + ArgumentFormatter.Format(actual); + + if (actualPointer is not null) + message += Environment.NewLine + " " + actualPointer; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal(message, ex.Message); + } + + [Theory] + // Nulls + [InlineData( + null, null, " null", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "int[] [1, 2, 3, 4, 5, $$ELLIPSIS$$]", null + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "int[] [1, 2, 3, 4, 5, $$ELLIPSIS$$]", + null, " null", null + )] + // Start of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, "↓ (pos 0)", "[1, 2, 3, 4, 5, $$ELLIPSIS$$]", + new[] { 99, 2, 3, 4, 5, 6, 7 }, "[99, 2, 3, 4, 5, $$ELLIPSIS$$]", "↑ (pos 0)" + )] + // Middle of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 3)", "[$$ELLIPSIS$$, 2, 3, 4, 5, 6, $$ELLIPSIS$$]", + new[] { 1, 2, 3, 99, 5, 6, 7 }, "[$$ELLIPSIS$$, 2, 3, 99, 5, 6, $$ELLIPSIS$$]", " ↑ (pos 3)" + )] + // End of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 6)", "[$$ELLIPSIS$$, 3, 4, 5, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 99 }, "[$$ELLIPSIS$$, 3, 4, 5, 6, 99]", " ↑ (pos 6)" + )] + // Overruns + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "[$$ELLIPSIS$$, 4, 5, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, "[$$ELLIPSIS$$, 4, 5, 6, 7, 8]", " ↑ (pos 7)" + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "[$$ELLIPSIS$$, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, "[$$ELLIPSIS$$, 6, 7, 8, 9, 10, $$ELLIPSIS$$]", " ↑ (pos 7)" + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, " ↓ (pos 7)", "[$$ELLIPSIS$$, 4, 5, 6, 7, 8]", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "[$$ELLIPSIS$$, 4, 5, 6, 7]", null + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, " ↓ (pos 7)", "[$$ELLIPSIS$$, 6, 7, 8, 9, 10, $$ELLIPSIS$$]", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "[$$ELLIPSIS$$, 6, 7]", null + )] + public void Truncation( + int[]? expected, + string? expectedPointer, + string expectedDisplay, + int[]? actual, + string actualDisplay, + string? actualPointer) + { + var message = "Assert.Equal() Failure: Collections differ"; + + if (expectedPointer is not null) + message += Environment.NewLine + " " + expectedPointer; + + message += + Environment.NewLine + "Expected: " + expectedDisplay + + Environment.NewLine + "Actual: " + actualDisplay; + + if (actualPointer is not null) + message += Environment.NewLine + " " + actualPointer; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal(message.Replace("$$ELLIPSIS$$", ArgumentFormatter.Ellipsis), ex.Message); + } + + [Fact] + public void SameValueDifferentType() + { + var ex = Record.Exception(() => Assert.Equal(new object[] { 1, 2, 3 }, [1, 2, 3L])); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 2, type System.Int32)" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3]" + Environment.NewLine + + " ↑ (pos 2, type System.Int64)", + ex.Message + ); + } + } + + public class ArraysWithComparer + { + // https://github.com/xunit/xunit/issues/2795 + [Fact] + public void CollectionItemIsEnumerable() + { + var actual = new EnumerableItem[] { new(0), new(2) }; + var expected = new EnumerableItem[] { new(1), new(3) }; + + Assert.Equal(expected, actual, new EnumerableItemComparer()); + } + + public class EnumerableItemComparer : IEqualityComparer + { + public bool Equals(EnumerableItem? x, EnumerableItem? y) => + x?.Value / 2 == y?.Value / 2; + + public int GetHashCode(EnumerableItem obj) => + throw new NotImplementedException(); + } + + public sealed class EnumerableItem(int value) : + IEnumerable + { + public int Value { get; } = value; + + public IEnumerator GetEnumerator() => + Enumerable.Repeat("", Value).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + } + } + + public class ArraysWithFunc + { + // https://github.com/xunit/xunit/issues/2795 + [Fact] + public void CollectionItemIsEnumerable() + { + var actual = new EnumerableItem[] { new(0), new(2) }; + var expected = new EnumerableItem[] { new(1), new(3) }; + + Assert.Equal(expected, actual, (x, y) => x.Value / 2 == y.Value / 2); + } + + public sealed class EnumerableItem(int value) : + IEnumerable + { + public int Value { get; } = value; + + public IEnumerator GetEnumerator() => + Enumerable.Repeat("", Value).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + } + } + + public class Collections + { + [Fact] + public static void Equal() + { + var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var actual = new List(expected); + + Assert.Equal(expected, actual); + } + + [Theory] + // Nulls + [InlineData( + null, null, " null", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "List [1, 2, 3, 4, 5, $$ELLIPSIS$$]", null + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "int[] [1, 2, 3, 4, 5, $$ELLIPSIS$$]", + null, " null", null + )] + // Start of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 0)", "int[] [1, 2, 3, 4, 5, $$ELLIPSIS$$]", + new[] { 99, 2, 3, 4, 5, 6, 7 }, "List [99, 2, 3, 4, 5, $$ELLIPSIS$$]", " ↑ (pos 0)" + )] + // Middle of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 3)", "int[] [$$ELLIPSIS$$, 2, 3, 4, 5, 6, $$ELLIPSIS$$]", + new[] { 1, 2, 3, 99, 5, 6, 7 }, "List [$$ELLIPSIS$$, 2, 3, 99, 5, 6, $$ELLIPSIS$$]", " ↑ (pos 3)" + )] + // End of array + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 6)", "int[] [$$ELLIPSIS$$, 3, 4, 5, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 99 }, "List [$$ELLIPSIS$$, 3, 4, 5, 6, 99]", " ↑ (pos 6)" + )] + // Overruns + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "int[] [$$ELLIPSIS$$, 4, 5, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, "List [$$ELLIPSIS$$, 4, 5, 6, 7, 8]", " ↑ (pos 7)" + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "int[] [$$ELLIPSIS$$, 6, 7]", + new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, "List [$$ELLIPSIS$$, 6, 7, 8, 9, 10, $$ELLIPSIS$$]", " ↑ (pos 7)" + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, " ↓ (pos 7)", "int[] [$$ELLIPSIS$$, 4, 5, 6, 7, 8]", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "List [$$ELLIPSIS$$, 4, 5, 6, 7]", null + )] + [InlineData( + new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, " ↓ (pos 7)", "int[] [$$ELLIPSIS$$, 6, 7, 8, 9, 10, $$ELLIPSIS$$]", + new[] { 1, 2, 3, 4, 5, 6, 7 }, "List [$$ELLIPSIS$$, 6, 7]", null + )] + public void NotEqual( + int[]? expected, + string? expectedPointer, + string expectedDisplay, + int[]? actualArray, + string actualDisplay, + string? actualPointer) + { + var actual = actualArray is null ? null : new List(actualArray); + var message = "Assert.Equal() Failure: Collections differ"; + + if (expectedPointer is not null) + message += Environment.NewLine + " " + expectedPointer; + + message += + Environment.NewLine + "Expected: " + expectedDisplay + + Environment.NewLine + "Actual: " + actualDisplay; + + if (actualPointer is not null) + message += Environment.NewLine + " " + actualPointer; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal(message.Replace("$$ELLIPSIS$$", ArgumentFormatter.Ellipsis), ex.Message); + } + } + + public class CollectionsWithComparer + { + [Fact] + public static void AlwaysFalse() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List { 1, 2, 3, 4, 5 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual, new IntComparer(false))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: int[] [1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: List [1, 2, 3, 4, 5]" + Environment.NewLine + + " ↑ (pos 0)", + ex.Message + ); + } + + [Fact] + public static void AlwaysTrue() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List { 0, 0, 0, 0, 0 }; + + Assert.Equal(expected, actual, new IntComparer(true)); + } + + class IntComparer(bool answer) : IEqualityComparer + { + public bool Equals(int x, int y) => + answer; + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + + // https://github.com/xunit/xunit/issues/2795 + [Fact] + public void CollectionItemIsEnumerable() + { + var actual = new List { new(0), new(2) }; + var expected = new List { new(1), new(3) }; + + Assert.Equal(expected, actual, new EnumerableItemComparer()); + } + + public class EnumerableItemComparer : IEqualityComparer + { + public bool Equals(EnumerableItem? x, EnumerableItem? y) => + x?.Value / 2 == y?.Value / 2; + + public int GetHashCode(EnumerableItem obj) => + throw new NotImplementedException(); + } + + public sealed class EnumerableItem(int value) : + IEnumerable + { + public int Value { get; } = value; + + public IEnumerator GetEnumerator() => + Enumerable.Repeat("", Value).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + } + + [Fact] + public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.Equal([1, 2], [1, 3], new ThrowingComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: [1, 2]" + Environment.NewLine + + "Actual: [1, 3]" + Environment.NewLine + + " ↑ (pos 0)", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + public class ThrowingComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + throw new DivideByZeroException(); + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + } + +#if !XUNIT_AOT // Embedded IEquatable cannot be done in Native AOT because of the reflection restrictions + + public class CollectionsWithEquatable + { + [Fact] + public void Equal() + { + var expected = new[] { new EquatableObject { Char = 'a' } }; + var actual = new[] { new EquatableObject { Char = 'a' } }; + + Assert.Equal(expected, actual); + } + + [Fact] + public void NotEqual() + { + var expected = new[] { new EquatableObject { Char = 'a' } }; + var actual = new[] { new EquatableObject { Char = 'b' } }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: [EquatableObject { Char = 'a' }]" + Environment.NewLine + + "Actual: [EquatableObject { Char = 'b' }]" + Environment.NewLine + + " ↑ (pos 0)", + ex.Message + ); + } + + public class EquatableObject : IEquatable + { + public char Char { get; set; } + + public bool Equals(EquatableObject? other) => + other != null && other.Char == Char; + + public override bool Equals(object? obj) => + Equals(obj as EquatableObject); + + public override int GetHashCode() => + Char.GetHashCode(); + } + } + +#endif // !XUNIT_AOT + + public class CollectionsWithFunc + { + [Fact] + public static void AlwaysFalse() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List { 1, 2, 3, 4, 5 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual, (x, y) => false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: int[] [1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: List [1, 2, 3, 4, 5]" + Environment.NewLine + + " ↑ (pos 0)", + ex.Message + ); + } + + [Fact] + public static void AlwaysTrue() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List { 0, 0, 0, 0, 0 }; + + Assert.Equal(expected, actual, (x, y) => true); + } + + // https://github.com/xunit/xunit/issues/2795 + [Fact] + public void CollectionItemIsEnumerable() + { + var expected = new List { new(1), new(3) }; + var actual = new List { new(0), new(2) }; + + Assert.Equal(expected, actual, (x, y) => x.Value / 2 == y.Value / 2); + } + + public sealed class EnumerableItem(int value) : + IEnumerable + { + public int Value { get; } = value; + + public IEnumerator GetEnumerator() => + Enumerable.Repeat("", Value).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + } + + [Fact] + public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException() + { + var ex = Record.Exception(() => + Assert.Equal( + [1, 2], + [1, 3], + static (e, a) => throw new DivideByZeroException() + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: [1, 2]" + Environment.NewLine + + "Actual: [1, 3]" + Environment.NewLine + + " ↑ (pos 0)", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + } + + public class Dictionaries + { + [Fact] + public static void InOrderDictionary() + { + var expected = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + var actual = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + + Assert.Equal(expected, actual); + } + + [Fact] + public static void OutOfOrderDictionary() + { + var expected = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + var actual = new Dictionary { { "b", 2 }, { "c", 3 }, { "a", 1 } }; + + Assert.Equal(expected, actual); + } + + [Fact] + public static void ExpectedLarger() + { + var expected = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + var actual = new Dictionary { { "a", 1 }, { "b", 2 } }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Dictionaries differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: [[a, 1], [b, 2], [c, 3]]" + Environment.NewLine + + "Actual: [[a, 1], [b, 2]]", +#else + "Expected: [[\"a\"] = 1, [\"b\"] = 2, [\"c\"] = 3]" + Environment.NewLine + + "Actual: [[\"a\"] = 1, [\"b\"] = 2]", +#endif + ex.Message + ); + } + + [Fact] + public static void ActualLarger() + { + var expected = new Dictionary { { "a", 1 }, { "b", 2 } }; + var actual = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Dictionaries differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: [[a, 1], [b, 2]]" + Environment.NewLine + + "Actual: [[a, 1], [b, 2], [c, 3]]", +#else + "Expected: [[\"a\"] = 1, [\"b\"] = 2]" + Environment.NewLine + + "Actual: [[\"a\"] = 1, [\"b\"] = 2, [\"c\"] = 3]", +#endif + ex.Message + ); + } + + [Fact] + public static void SomeKeysDiffer() + { + var expected = new Dictionary + { + ["a"] = 1, + ["be"] = 2, + ["c"] = 3, + ["d"] = 4, + ["e"] = 5, + ["f"] = 6, + }; + var actual = new Dictionary + { + ["a"] = 1, + ["ba"] = 2, + ["c"] = 3, + ["d"] = 4, + ["e"] = 5, + ["f"] = 6, + }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Dictionaries differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: [[a, 1], [be, 2], [c, 3], [d, 4], [e, 5], {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + $"Actual: [[a, 1], [ba, 2], [c, 3], [d, 4], [e, 5], {ArgumentFormatter.Ellipsis}]", +#else + $"Expected: [[\"a\"] = 1, [\"be\"] = 2, [\"c\"] = 3, [\"d\"] = 4, [\"e\"] = 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + $"Actual: [[\"a\"] = 1, [\"ba\"] = 2, [\"c\"] = 3, [\"d\"] = 4, [\"e\"] = 5, {ArgumentFormatter.Ellipsis}]", +#endif + ex.Message + ); + } + + [Fact] + public static void WithCollectionValues_Equal() + { +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + + // Different concrete collection types in the value slot, per https://github.com/xunit/xunit/issues/2850 + var expected = new Dictionary> + { + ["toAddresses"] = new List { "test1@example.com" }, + ["ccAddresses"] = new List { "test2@example.com" }, + }; + var actual = new Dictionary> + { + ["toAddresses"] = new string[] { "test1@example.com" }, + ["ccAddresses"] = new string[] { "test2@example.com" }, + }; + +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + Assert.Equal(expected, actual); + } + + [Fact] + public static void WithCollectionValues_NotEqual() + { +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + + // Different concrete collection types in the value slot, per https://github.com/xunit/xunit/issues/2850 + var expected = new Dictionary> + { + ["toAddresses"] = new List { "test1@example.com" }, + ["ccAddresses"] = new List { "test2@example.com" }, + }; + var actual = new Dictionary> + { + ["toAddresses"] = new string[] { "test1@example.com" }, + ["ccAddresses"] = new string[] { "test3@example.com" }, + }; + +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Dictionaries differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: [[toAddresses, System.Collections.Generic.List`1[System.String]], [ccAddresses, System.Collections.Generic.List`1[System.String]]]" + Environment.NewLine + + "Actual: [[toAddresses, System.String[]], [ccAddresses, System.String[]]]", +#else + "Expected: [[\"toAddresses\"] = [\"test1@example.com\"], [\"ccAddresses\"] = [\"test2@example.com\"]]" + Environment.NewLine + + "Actual: [[\"toAddresses\"] = [\"test1@example.com\"], [\"ccAddresses\"] = [\"test3@example.com\"]]", +#endif + ex.Message + ); + } + +#if !XUNIT_AOT // Embedded IEquatable cannot be done in Native AOT because of the reflection restrictions + + [Fact] + public void EquatableValues_Equal() + { + var expected = new Dictionary { { "Key1", new() { Char = 'a' } } }; + var actual = new Dictionary { { "Key1", new() { Char = 'a' } } }; + + Assert.Equal(expected, actual); + } + + [Fact] + public void EquatableValues_NotEqual() + { + var expected = new Dictionary { { "Key1", new() { Char = 'a' } } }; + var actual = new Dictionary { { "Key1", new() { Char = 'b' } } }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Dictionaries differ" + Environment.NewLine + + "Expected: [[\"Key1\"] = EquatableObject { Char = 'a' }]" + Environment.NewLine + + "Actual: [[\"Key1\"] = EquatableObject { Char = 'b' }]", + ex.Message + ); + } + + public class EquatableObject : IEquatable + { + public char Char { get; set; } + + public bool Equals(EquatableObject? other) => + other != null && other.Char == Char; + + public override bool Equals(object? obj) => + Equals(obj as EquatableObject); + + public override int GetHashCode() => + Char.GetHashCode(); + } + +#endif // !XUNIT_AOT + + [Fact] + public void ComplexEmbeddedValues_Equal() + { + var expected = new Dictionary() + { + ["key"] = new Dictionary() + { + ["key"] = new List>() + { + new() + { + ["key"] = new List { "value" } + } + } + } + }; + var actual = new Dictionary() + { + ["key"] = new Dictionary() + { + ["key"] = new List>() + { + new() + { + ["key"] = new List { "value" } + } + } + } + }; + + Assert.Equal(expected, actual); + } + + [Fact] + public void ComplexEmbeddedValues_NotEqual() + { + var expected = new Dictionary() + { + ["key"] = new Dictionary() + { + ["key"] = new List>() + { + new() + { + ["key"] = new List { "value1" } + } + } + } + }; + var actual = new Dictionary() + { + ["key"] = new Dictionary() + { + ["key"] = new List>() + { + new() + { + ["key"] = new List { "value2" } + } + } + } + }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Dictionaries differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: [[key, System.Collections.Generic.Dictionary`2[System.String,System.Object]]]" + Environment.NewLine + + "Actual: [[key, System.Collections.Generic.Dictionary`2[System.String,System.Object]]]", +#else + "Expected: [[\"key\"] = [[\"key\"] = [[[\"key\"] = [\"value1\"]]]]]" + Environment.NewLine + + "Actual: [[\"key\"] = [[\"key\"] = [[[\"key\"] = [\"value2\"]]]]]", +#endif + ex.Message + ); + } + } + + public class Sets + { + [Fact] + public void Equal() + { + var expected = new HashSet { 42, 2112 }; + var actual = new HashSet { 2112, 42 }; + + Assert.Equal(expected, actual); + } + + [Fact] + public void Equal_WithInternalComparer() + { + var comparer = new BitArrayComparer(); + var expected = new HashSet(comparer) { new([true, false]) }; + var actual = new HashSet(comparer) { new([true, false]) }; + + Assert.Equal(expected, actual); + } + + [Fact] + public void Equal_WithExternalComparer() + { + var expected = new HashSet { new([true, false]) }; + var actual = new HashSet { new([true, false]) }; + + Assert.Equal(expected, actual, new BitArrayComparer()); + } + + [Fact] + public void NotEqual() + { + var expected = new HashSet { 42, 2112 }; + var actual = new HashSet { 2600, 42 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: HashSets differ" + Environment.NewLine + + "Expected: [42, 2112]" + Environment.NewLine + + "Actual: [2600, 42]", + ex.Message + ); + } + + [Fact] + public void NotEqual_WithInternalComparer() + { + var comparer = new BitArrayComparer(); + var expected = new HashSet(comparer) { new([true, false]) }; + var actual = new HashSet(comparer) { new([true, true]) }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: HashSets differ" + Environment.NewLine + + "Expected: [[True, False]]" + Environment.NewLine + + "Actual: [[True, True]]", + ex.Message + ); + } + + [Fact] + public void NotEqual_WithExternalComparer() + { + var expected = new HashSet { new([true, false]) }; + var actual = new HashSet { new([true, true]) }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual, new BitArrayComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: HashSets differ" + Environment.NewLine + + "Expected: [[True, False]]" + Environment.NewLine + + "Actual: [[True, True]]", + ex.Message + ); + } + + public class BitArrayComparer : IEqualityComparer + { + public bool Equals( + BitArray? x, + BitArray? y) => + ToBitString(x) == ToBitString(y); + + public int GetHashCode(BitArray obj) => + ToBitString(obj).GetHashCode(); + + static string ToBitString(BitArray? bitArray) + { + if (bitArray is null) + return string.Empty; + + var sb = new StringBuilder(bitArray.Length); + + for (int idx = 0; idx < bitArray.Length; ++idx) + sb.Append(bitArray[idx] ? '1' : '0'); + + return sb.ToString(); + } + } + } + } + + public class NotEmpty + { + [Fact] + public static void EmptyContainer() + { + var list = new List(); + + var ex = Record.Exception(() => Assert.NotEmpty(list)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEmpty() Failure: Collection was empty", + ex.Message + ); + } + + [Fact] + public static void NonEmptyContainer() + { + var list = new List { 42 }; + + Assert.NotEmpty(list); + } + + [Fact] + public static void EnumeratorDisposed() + { + var enumerator = new SpyEnumerator(Enumerable.Range(0, 1)); + + Assert.NotEmpty(enumerator); + + Assert.True(enumerator.IsDisposed); + } + } + + public class NotEqual + { + public class Null + { + [Fact] + public static void BothNull() + { + var expected = default(IEnumerable); + var actual = default(IEnumerable); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not null" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + + [Fact] + public static void EmptyExpectedNullActual() + { + var expected = new int[0]; + var actual = default(IEnumerable); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public static void NullExpectedEmptyActual() + { + var expected = default(IEnumerable); + var actual = new int[0]; + + Assert.NotEqual(expected, actual); + } + } + + public class Arrays + { + [Fact] + public static void Equal() + { + string[] expected = ["@", "a", "ab", "b"]; + string[] actual = ["@", "a", "ab", "b"]; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [\"@\", \"a\", \"ab\", \"b\"]" + Environment.NewLine + + "Actual: [\"@\", \"a\", \"ab\", \"b\"]", + ex.Message + ); + } + + [Fact] + public static void EmbeddedArrays_Equal() + { + string[][] expected = [["@", "a"], ["ab", "b"]]; + string[][] actual = [["@", "a"], ["ab", "b"]]; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [[\"@\", \"a\"], [\"ab\", \"b\"]]" + Environment.NewLine + + "Actual: [[\"@\", \"a\"], [\"ab\", \"b\"]]", + ex.Message + ); + } + + [Fact] + public static void NotEqual() + { + IEnumerable expected = [1, 2, 3]; + IEnumerable actual = [1, 2, 4]; + + Assert.NotEqual(expected, actual); + } + + [Fact] + public static void SameValueDifferentType() + { + Assert.NotEqual([1, 2, 3], [1, 2, 3L]); + } + } + + public class Collections + { + [Fact] + public static void Equal() + { + var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var actual = new List(expected); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + $"Expected: Not int[] [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + $"Actual: List [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]", + ex.Message + ); + } + + [Fact] + public static void NotEqual() + { + var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var actual = new List { 1, 2, 3, 4, 0, 6, 7, 8, 9, 10 }; + + Assert.NotEqual(expected, actual); + } + } + + public class CollectionsWithComparer + { + [Fact] + public static void AlwaysFalse() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List { 1, 2, 3, 4, 5 }; + + Assert.NotEqual(expected, actual, new IntComparer(false)); + } + + [Fact] + public static void AlwaysTrue() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List { 0, 0, 0, 0, 0 }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual, new IntComparer(true))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not int[] [1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: List [0, 0, 0, 0, 0]", + ex.Message + ); + } + + class IntComparer(bool answer) : + IEqualityComparer + { + public bool Equals(int x, int y) => + answer; + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + + [Fact] + public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.NotEqual([1, 2], [1, 2], new ThrowingComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: Not [1, 2]" + Environment.NewLine + + "Actual: [1, 2]" + Environment.NewLine + + " ↑ (pos 0)", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + public class ThrowingComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + throw new DivideByZeroException(); + + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + } + +#if !XUNIT_AOT // Embedded IEquatable cannot be done in Native AOT because of the reflection restrictions + + public class CollectionsWithEquatable + { + [Fact] + public void Equal() + { + var expected = new[] { new EquatableObject { Char = 'a' } }; + var actual = new[] { new EquatableObject { Char = 'a' } }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [EquatableObject { Char = 'a' }]" + Environment.NewLine + + "Actual: [EquatableObject { Char = 'a' }]", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + var expected = new[] { new EquatableObject { Char = 'a' } }; + var actual = new[] { new EquatableObject { Char = 'b' } }; + + Assert.NotEqual(expected, actual); + } + + public class EquatableObject : IEquatable + { + public char Char { get; set; } + + public bool Equals(EquatableObject? other) => + other != null && other.Char == Char; + + public override bool Equals(object? obj) => + Equals(obj as EquatableObject); + + public override int GetHashCode() => + Char.GetHashCode(); + } + } + +#endif // !XUNIT_AOT + + public class CollectionsWithFunc + { + [Fact] + public static void AlwaysFalse() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List { 1, 2, 3, 4, 5 }; + + Assert.NotEqual(expected, actual, (x, y) => false); + } + + [Fact] + public static void AlwaysTrue() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new List { 0, 0, 0, 0, 0 }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual, (x, y) => true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not int[] [1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: List [0, 0, 0, 0, 0]", + ex.Message + ); + } + + [Fact] + public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException() + { + var ex = Record.Exception(() => + Assert.NotEqual( + [1, 2], + [1, 2], + static (e, a) => throw new DivideByZeroException() + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: Not [1, 2]" + Environment.NewLine + + "Actual: [1, 2]" + Environment.NewLine + + " ↑ (pos 0)", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + } + + public class Dictionaries + { + [Fact] + public static void InOrderDictionary() + { + var expected = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + var actual = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Dictionaries are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [[a, 1], [b, 2], [c, 3]]" + Environment.NewLine + + "Actual: [[a, 1], [b, 2], [c, 3]]", +#else + "Expected: Not [[\"a\"] = 1, [\"b\"] = 2, [\"c\"] = 3]" + Environment.NewLine + + "Actual: [[\"a\"] = 1, [\"b\"] = 2, [\"c\"] = 3]", +#endif + ex.Message + ); + } + + [Fact] + public static void OutOfOrderDictionary() + { + var expected = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + var actual = new Dictionary { { "b", 2 }, { "c", 3 }, { "a", 1 } }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Dictionaries are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [[a, 1], [b, 2], [c, 3]]" + Environment.NewLine + + "Actual: [[b, 2], [c, 3], [a, 1]]", +#else + "Expected: Not [[\"a\"] = 1, [\"b\"] = 2, [\"c\"] = 3]" + Environment.NewLine + + "Actual: [[\"b\"] = 2, [\"c\"] = 3, [\"a\"] = 1]", +#endif + ex.Message + ); + } + + [Fact] + public static void ExpectedLarger() + { + var expected = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + var actual = new Dictionary { { "a", 1 }, { "b", 2 } }; + + Assert.NotEqual(expected, actual); + } + + [Fact] + public static void ActualLarger() + { + var expected = new Dictionary { { "a", 1 }, { "b", 2 } }; + var actual = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + + Assert.NotEqual(expected, actual); + } + + [Fact] + public static void SomeKeysDiffer() + { + var expected = new Dictionary + { + ["a"] = 1, + ["be"] = 2, + ["c"] = 3, + ["d"] = 4, + ["e"] = 5, + ["f"] = 6, + }; + var actual = new Dictionary + { + ["a"] = 1, + ["ba"] = 2, + ["c"] = 3, + ["d"] = 4, + ["e"] = 5, + ["f"] = 6, + }; + + Assert.NotEqual(expected, actual); + } + + [Fact] + public static void WithCollectionValues_Equal() + { +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + + // Different concrete collection types in the value slot, per https://github.com/xunit/xunit/issues/2850 + var expected = new Dictionary> + { + ["toAddresses"] = new List { "test1@example.com" }, + ["ccAddresses"] = new List { "test2@example.com" }, + }; + var actual = new Dictionary> + { + ["toAddresses"] = new string[] { "test1@example.com" }, + ["ccAddresses"] = new string[] { "test2@example.com" }, + }; + +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Dictionaries are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [[toAddresses, System.Collections.Generic.List`1[System.String]], [ccAddresses, System.Collections.Generic.List`1[System.String]]]" + Environment.NewLine + + "Actual: [[toAddresses, System.String[]], [ccAddresses, System.String[]]]", +#else + "Expected: Not [[\"toAddresses\"] = [\"test1@example.com\"], [\"ccAddresses\"] = [\"test2@example.com\"]]" + Environment.NewLine + + "Actual: [[\"toAddresses\"] = [\"test1@example.com\"], [\"ccAddresses\"] = [\"test2@example.com\"]]", +#endif + ex.Message + ); + } + + [Fact] + public static void WithCollectionValues_NotEqual() + { +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + + // Different concrete collection types in the value slot, per https://github.com/xunit/xunit/issues/2850 + var expected = new Dictionary> + { + ["toAddresses"] = new List { "test1@example.com" }, + ["ccAddresses"] = new List { "test2@example.com" }, + }; + var actual = new Dictionary> + { + ["toAddresses"] = new string[] { "test1@example.com" }, + ["ccAddresses"] = new string[] { "test3@example.com" }, + }; + +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + Assert.NotEqual(expected, actual); + } + +#if !XUNIT_AOT // Embedded IEquatable cannot be done in Native AOT because of the reflection restrictions + + [Fact] + public void EquatableValues_Equal() + { + var expected = new Dictionary { { "Key1", new() { Char = 'a' } } }; + var actual = new Dictionary { { "Key1", new() { Char = 'a' } } }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Dictionaries are equal" + Environment.NewLine + + "Expected: Not [[\"Key1\"] = EquatableObject { Char = 'a' }]" + Environment.NewLine + + "Actual: [[\"Key1\"] = EquatableObject { Char = 'a' }]", + ex.Message + ); + } + + [Fact] + public void EquatableValues_NotEqual() + { + var expected = new Dictionary { { "Key1", new() { Char = 'a' } } }; + var actual = new Dictionary { { "Key1", new() { Char = 'b' } } }; + + Assert.NotEqual(expected, actual); + } + + public class EquatableObject : IEquatable + { + public char Char { get; set; } + + public bool Equals(EquatableObject? other) => + other != null && other.Char == Char; + + public override bool Equals(object? obj) => + Equals(obj as EquatableObject); + + public override int GetHashCode() => + Char.GetHashCode(); + } + +#endif // !XUNIT_AOT + + [Fact] + public void ComplexEmbeddedValues_Equal() + { + var expected = new Dictionary() + { + ["key"] = new Dictionary() + { + ["key"] = new List>() + { + new() + { + ["key"] = new List { "value" } + } + } + } + }; + var actual = new Dictionary() + { + ["key"] = new Dictionary() + { + ["key"] = new List>() + { + new() + { + ["key"] = new List { "value" } + } + } + } + }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Dictionaries are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [[key, System.Collections.Generic.Dictionary`2[System.String,System.Object]]]" + Environment.NewLine + + "Actual: [[key, System.Collections.Generic.Dictionary`2[System.String,System.Object]]]", +#else + "Expected: Not [[\"key\"] = [[\"key\"] = [[[\"key\"] = [\"value\"]]]]]" + Environment.NewLine + + "Actual: [[\"key\"] = [[\"key\"] = [[[\"key\"] = [\"value\"]]]]]", +#endif + ex.Message + ); + } + + [Fact] + public void ComplexEmbeddedValues_NotEqual() + { + var expected = new Dictionary() + { + ["key"] = new Dictionary() + { + ["key"] = new List>() + { + new() + { + ["key"] = new List { "value1" } + } + } + } + }; + var actual = new Dictionary() + { + ["key"] = new Dictionary() + { + ["key"] = new List>() + { + new() + { + ["key"] = new List { "value2" } + } + } + } + }; + + Assert.NotEqual(expected, actual); + } + } + + public class Sets + { + [Fact] + public void Equal() + { + var expected = new HashSet { 42, 2112 }; + var actual = new HashSet { 2112, 42 }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: HashSets are equal" + Environment.NewLine + + "Expected: Not [42, 2112]" + Environment.NewLine + + "Actual: [2112, 42]", + ex.Message + ); + } + + [Fact] + public void Equal_WithInternalComparer() + { + var comparer = new BitArrayComparer(); + var expected = new HashSet(comparer) { new([true, false]) }; + var actual = new HashSet(comparer) { new([true, false]) }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: HashSets are equal" + Environment.NewLine + + "Expected: Not [[True, False]]" + Environment.NewLine + + "Actual: [[True, False]]", + ex.Message + ); + } + + [Fact] + public void Equal_WithExternalComparer() + { + var expected = new HashSet { new([true, false]) }; + var actual = new HashSet { new([true, false]) }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual, new BitArrayComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: HashSets are equal" + Environment.NewLine + + "Expected: Not [[True, False]]" + Environment.NewLine + + "Actual: [[True, False]]", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + var expected = new HashSet { 42, 2112 }; + var actual = new HashSet { 2600, 42 }; + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void NotEqual_WithInternalComparer() + { + var comparer = new BitArrayComparer(); + var expected = new HashSet(comparer) { new([true, false]) }; + var actual = new HashSet(comparer) { new([true, true]) }; + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void NotEqual_WithExternalComparer() + { + var expected = new HashSet { new([true, false]) }; + var actual = new HashSet { new([true, true]) }; + + Assert.NotEqual(expected, actual, new BitArrayComparer()); + } + + public class BitArrayComparer : IEqualityComparer + { + public bool Equals( + BitArray? x, + BitArray? y) => + ToBitString(x) == ToBitString(y); + + public int GetHashCode(BitArray obj) => + ToBitString(obj).GetHashCode(); + + static string ToBitString(BitArray? bitArray) + { + if (bitArray is null) + return string.Empty; + + var sb = new StringBuilder(bitArray.Length); + + for (int idx = 0; idx < bitArray.Length; ++idx) + sb.Append(bitArray[idx] ? '1' : '0'); + + return sb.ToString(); + } + } + } + } + + public class Single_NonGeneric + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.Single(null!)); + } + + [Fact] + public static void EmptyCollection() + { + var collection = new ArrayList(); + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal("Assert.Single() Failure: The collection was empty", ex.Message); + } + + [Fact] + public static void SingleItemCollection() + { + var collection = new ArrayList { "Hello" }; + + var item = Assert.Single(collection); + + Assert.Equal("Hello", item); + } + + [Fact] + public static void MultiItemCollection() + { + var collection = new ArrayList { "Hello", "World" }; + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 items" + Environment.NewLine + + "Collection: [\"Hello\", \"World\"]", + ex.Message + ); + } + + [Fact] + public static void Truncation() + { + var collection = new ArrayList { 1, 2, 3, 4, 5, 6, 7 }; + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 7 items" + Environment.NewLine + + $"Collection: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]", + ex.Message + ); + } + } + + public class Single_NonGeneric_WithObject + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.Single(null!, null)); + } + + [Fact] + public static void SingleMatch() + { + IEnumerable collection = new ArrayList { "Hello", "World" }; + + Assert.Single(collection, "Hello"); + } + + [Fact] + public static void SingleMatch_Null() + { + IEnumerable collection = new ArrayList { "Hello", "World!", null }; + + Assert.Single(collection, null); + } + + [Fact] + public static void NoMatches() + { + IEnumerable collection = new ArrayList { "Hello", "World" }; + + var ex = Record.Exception(() => Assert.Single(collection, "foo")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection did not contain any matching items" + Environment.NewLine + + "Expected: \"foo\"" + Environment.NewLine + + "Collection: [\"Hello\", \"World\"]", + ex.Message + ); + } + + [Fact] + public static void TooManyMatches() + { + var collection = new ArrayList { "Hello", "World", "Hello" }; + + var ex = Record.Exception(() => Assert.Single(collection, "Hello")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 matching items" + Environment.NewLine + + "Expected: \"Hello\"" + Environment.NewLine + + "Collection: [\"Hello\", \"World\", \"Hello\"]" + Environment.NewLine + + "Match indices: 0, 2", + ex.Message + ); + } + + [Fact] + public static void Truncation() + { + var collection = new ArrayList { 1, 2, 3, 4, 5, 6, 7, 8, 4 }; + + var ex = Record.Exception(() => Assert.Single(collection, 4)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 matching items" + Environment.NewLine + + "Expected: 4" + Environment.NewLine + + $"Collection: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Match indices: 3, 8", + ex.Message + ); + } + } + + public class Single_Generic + { + [Fact] + public static void GuardClause() + { + Assert.Throws("collection", () => Assert.Single(default(IEnumerable)!)); + } + + [Fact] + public static void EmptyCollection() + { + var collection = new object[0]; + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal("Assert.Single() Failure: The collection was empty", ex.Message); + } + + [Fact] + public static void SingleItemCollection() + { + var collection = new[] { "Hello" }; + + var item = Assert.Single(collection); + + Assert.Equal("Hello", item); + } + + [Fact] + public static void MultiItemCollection() + { + var collection = new[] { "Hello", "World" }; + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 items" + Environment.NewLine + + "Collection: [\"Hello\", \"World\"]", + ex.Message + ); + } + + [Fact] + public static void Truncation() + { + var collection = new[] { 1, 2, 3, 4, 5, 6, 7 }; + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 7 items" + Environment.NewLine + + $"Collection: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]", + ex.Message + ); + } + + [Fact] + public static void StringAsCollection_Match() + { + var collection = "H"; + + var value = Assert.Single(collection); + + Assert.Equal('H', value); + } + + [Fact] + public static void StringAsCollection_NoMatch() + { + var collection = "Hello"; + + var ex = Record.Exception(() => Assert.Single(collection)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 5 items" + Environment.NewLine + + "Collection: ['H', 'e', 'l', 'l', 'o']", + ex.Message + ); + } + } + + public class Single_Generic_WithPredicate + { + [Fact] + public static void GuardClauses() + { + Assert.Throws("collection", () => Assert.Single(default(IEnumerable)!, _ => true)); + Assert.Throws("predicate", () => Assert.Single(new object[0], null!)); + } + + [Fact] + public static void SingleMatch() + { + var collection = new[] { "Hello", "World" }; + + var result = Assert.Single(collection, item => item.StartsWith("H", StringComparison.InvariantCulture)); + + Assert.Equal("Hello", result); + } + + [Fact] + public static void NoMatches() + { + var collection = new[] { "Hello", "World" }; + + var ex = Record.Exception(() => Assert.Single(collection, item => false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection did not contain any matching items" + Environment.NewLine + + "Expected: (predicate expression)" + Environment.NewLine + + "Collection: [\"Hello\", \"World\"]", + ex.Message + ); + } + + [Fact] + public static void TooManyMatches() + { + var collection = new[] { "Hello", "World" }; + + var ex = Record.Exception(() => Assert.Single(collection, item => true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 matching items" + Environment.NewLine + + "Expected: (predicate expression)" + Environment.NewLine + + "Collection: [\"Hello\", \"World\"]" + Environment.NewLine + + "Match indices: 0, 1", + ex.Message + ); + } + + [Fact] + public static void Truncation() + { + var collection = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 4 }; + + var ex = Record.Exception(() => Assert.Single(collection, item => item == 4)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection contained 2 matching items" + Environment.NewLine + + "Expected: (predicate expression)" + Environment.NewLine + + $"Collection: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Match indices: 3, 8", + ex.Message + ); + } + + [Fact] + public static void StringAsCollection_Match() + { + var collection = "H"; + + var value = Assert.Single(collection, c => c != 'Q'); + + Assert.Equal('H', value); + } + + [Fact] + public static void StringAsCollection_NoMatch() + { + var collection = "H"; + + var ex = Record.Exception(() => Assert.Single(collection, c => c == 'Q')); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Single() Failure: The collection did not contain any matching items" + Environment.NewLine + + "Expected: (predicate expression)" + Environment.NewLine + + "Collection: ['H']", + ex.Message + ); + } + } + + sealed class SpyEnumerator(IEnumerable enumerable) : + IEnumerable, IEnumerator + { + IEnumerator? innerEnumerator = enumerable.GetEnumerator(); + + public T Current => + GuardNotNull("Tried to get Current on a disposed enumerator", innerEnumerator).Current; + + object? IEnumerator.Current => + GuardNotNull("Tried to get Current on a disposed enumerator", innerEnumerator).Current; + + public bool IsDisposed => + innerEnumerator is null; + + public IEnumerator GetEnumerator() => + this; + + IEnumerator IEnumerable.GetEnumerator() => + this; + + public bool MoveNext() => + GuardNotNull("Tried to call MoveNext() on a disposed enumerator", innerEnumerator).MoveNext(); + + public void Reset() => throw new NotImplementedException(); + + public void Dispose() + { + innerEnumerator?.Dispose(); + innerEnumerator = null; + } + + /// + static T2 GuardNotNull( + string message, + [NotNull] T2? value) + where T2 : class + { + if (value is null) + throw new InvalidOperationException(message); + + return value; + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/DictionaryAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/DictionaryAssertsTests.cs new file mode 100644 index 00000000000..f5570f12662 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/DictionaryAssertsTests.cs @@ -0,0 +1,103 @@ +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using Xunit; +using Xunit.Sdk; + +public class DictionaryAssertsTests +{ + public class Contains + { + [Fact] + public static void KeyInDictionary() + { + var dictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["forty-two"] = 42 + }; + + Assert.Equal(42, Assert.Contains("FORTY-two", dictionary)); + Assert.Equal(42, Assert.Contains("FORTY-two", new ReadOnlyDictionary(dictionary))); + Assert.Equal(42, Assert.Contains("FORTY-two", (IDictionary)dictionary)); + Assert.Equal(42, Assert.Contains("FORTY-two", (IReadOnlyDictionary)dictionary)); + Assert.Equal(42, Assert.Contains("FORTY-two", dictionary.ToImmutableDictionary(StringComparer.InvariantCultureIgnoreCase))); + Assert.Equal(42, Assert.Contains("FORTY-two", (IImmutableDictionary)dictionary.ToImmutableDictionary(StringComparer.InvariantCultureIgnoreCase))); + } + + [Fact] + public static void KeyNotInDictionary() + { + var dictionary = new Dictionary() + { + ["eleventeen"] = 110 + }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Key not found in dictionary" + Environment.NewLine + + "Keys: [\"eleventeen\"]" + Environment.NewLine + + "Not found: \"FORTY-two\"", + ex.Message + ); + } + + assertFailure(() => Assert.Contains("FORTY-two", dictionary)); + assertFailure(() => Assert.Contains("FORTY-two", new ReadOnlyDictionary(dictionary))); + assertFailure(() => Assert.Contains("FORTY-two", (IDictionary)dictionary)); + assertFailure(() => Assert.Contains("FORTY-two", (IReadOnlyDictionary)dictionary)); + assertFailure(() => Assert.Contains("FORTY-two", dictionary.ToImmutableDictionary())); + assertFailure(() => Assert.Contains("FORTY-two", (IImmutableDictionary)dictionary.ToImmutableDictionary())); + } + } + + public class DoesNotContain + { + [Fact] + public static void KeyNotInDictionary() + { + var dictionary = new Dictionary() + { + ["eleventeen"] = 110 + }; + + Assert.DoesNotContain("FORTY-two", dictionary); + Assert.DoesNotContain("FORTY-two", new ReadOnlyDictionary(dictionary)); + Assert.DoesNotContain("FORTY-two", (IDictionary)dictionary); + Assert.DoesNotContain("FORTY-two", (IReadOnlyDictionary)dictionary); + Assert.DoesNotContain("FORTY-two", dictionary.ToImmutableDictionary()); + Assert.DoesNotContain("FORTY-two", (IImmutableDictionary)dictionary.ToImmutableDictionary()); + } + + [Fact] + public static void KeyInDictionary() + { + var dictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["forty-two"] = 42 + }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Key found in dictionary" + Environment.NewLine + + "Keys: [\"forty-two\"]" + Environment.NewLine + + "Found: \"FORTY-two\"", + ex.Message + ); + } + + assertFailure(() => Assert.DoesNotContain("FORTY-two", dictionary)); + assertFailure(() => Assert.DoesNotContain("FORTY-two", new ReadOnlyDictionary(dictionary))); + assertFailure(() => Assert.DoesNotContain("FORTY-two", (IDictionary)dictionary)); + assertFailure(() => Assert.DoesNotContain("FORTY-two", (IReadOnlyDictionary)dictionary)); + assertFailure(() => Assert.DoesNotContain("FORTY-two", dictionary.ToImmutableDictionary(StringComparer.InvariantCultureIgnoreCase))); + assertFailure(() => Assert.DoesNotContain("FORTY-two", (IImmutableDictionary)dictionary.ToImmutableDictionary(StringComparer.InvariantCultureIgnoreCase))); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EqualityAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EqualityAssertsTests.cs new file mode 100644 index 00000000000..8f2b647c3ad --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EqualityAssertsTests.cs @@ -0,0 +1,4890 @@ +#pragma warning disable CA1512 // Use ArgumentOutOfRangeException throw helper + +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using Xunit; +using Xunit.Sdk; + +public class EqualityAssertsTests +{ + public class Equal + { + public class ReferenceEquality + { + // https://github.com/xunit/xunit/issues/2271 + [Fact] + public void TwoIdenticalReferencesShouldBeEqual() + { + Field x = new Field(); + + Assert.Equal(x, x); + } + + sealed class Field : IReadOnlyList + { + Field IReadOnlyList.this[int index] + { + get + { + if (index != 0) + throw new ArgumentOutOfRangeException(nameof(index)); + + return this; + } + } + + int IReadOnlyCollection.Count => 1; + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + } + } + } + + public class Intrinsics + { + [Fact] + public void Equal() + { + Assert.Equal(42, 42); + } + + [Fact] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(42, 2112)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + + [Fact] + public void StringsPassViaObjectEqualAreNotFormattedOrTruncated() + { + var ex = Record.Exception( + () => Assert.Equal( + $"This is a long{Environment.NewLine}string with{Environment.NewLine}new lines" as object, + $"This is a long{Environment.NewLine}string with embedded{Environment.NewLine}new lines" + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: This is a long" + Environment.NewLine + + " string with" + Environment.NewLine + + " new lines" + Environment.NewLine + + "Actual: This is a long" + Environment.NewLine + + " string with embedded" + Environment.NewLine + + " new lines", + ex.Message + ); + } + } + + public class WithComparer + { + [Fact] + public void GuardClause() + { + Assert.Throws("comparer", () => Assert.Equal(1, 2, default(IEqualityComparer)!)); + } + + [Fact] + public void Equal() + { + Assert.Equal(42, 21, new Comparer(true)); + } + + [Fact] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(42, 42, new Comparer(false))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 42", + ex.Message + ); + } + + class Comparer(bool result) : + IEqualityComparer + { + readonly bool result = result; + + public bool Equals(T? x, T? y) => result; + + public int GetHashCode(T obj) => throw new NotImplementedException(); + } + + [Fact] + public void NonEnumerable_WithThrow_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.Equal(42, 2112, new ThrowingIntComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + public class ThrowingIntComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + throw new DivideByZeroException(); + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + + [Fact] + public void Enumerable_WithThrow_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.Equal([1, 2], [1, 3], new ThrowingEnumerableComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: [1, 2]" + Environment.NewLine + + "Actual: [1, 3]", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + public class ThrowingEnumerableComparer : IEqualityComparer> + { + public bool Equals(IEnumerable? x, IEnumerable? y) => + throw new DivideByZeroException(); + public int GetHashCode(IEnumerable obj) => + throw new NotImplementedException(); + } + } + + public class WithFunc + { + [Fact] + public void GuardClause() + { + Assert.Throws("comparer", () => Assert.Equal(1, 2, default(Func)!)); + } + + [Fact] + public void Equal() + { + Assert.Equal(42, 21, (x, y) => true); + } + + [Fact] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(42, 42, (x, y) => false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 42", + ex.Message + ); + } + + [Fact] + public void NonEnumerable_WithThrow_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.Equal(42, 2112, (e, a) => throw new DivideByZeroException())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + [Fact] + public void Enumerable_WithThrow_RecordsInnerException() + { + var ex = Record.Exception( + () => Assert.Equal( + [1, 2], + [1, 3], + (IEnumerable e, IEnumerable a) => throw new DivideByZeroException() + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: [1, 2]" + Environment.NewLine + + "Actual: [1, 3]", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + } + + public class Comparable + { + [Fact] + public void Equal() + { + var obj1 = new SpyComparable(0); + var obj2 = new SpyComparable(0); + + Assert.Equal(obj1, obj2); + Assert.True(obj1.CompareCalled); + } + + [Fact] + public void NotEqual() + { + var obj1 = new SpyComparable(-1); + var obj2 = new SpyComparable(0); + + var ex = Record.Exception(() => Assert.Equal(obj1, obj2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: SpyComparable {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: SpyComparable {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: SpyComparable { CompareCalled = True }" + Environment.NewLine + + "Actual: SpyComparable { CompareCalled = False }", +#endif + ex.Message + ); + } + + [Fact] + public void NonGeneric_SameType_Equal() + { + var expected = new MultiComparable(1); + var actual = new MultiComparable(1); + + Assert.Equal(expected, actual); + Assert.Equal(expected, (IComparable)actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void NonGeneric_SameType_NotEqual() + { + var expected = new MultiComparable(1); + var actual = new MultiComparable(2); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: MultiComparable {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: MultiComparable {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: MultiComparable { Value = 1 }" + Environment.NewLine + + "Actual: MultiComparable { Value = 2 }", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (IComparable)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + + [Fact] + public void NonGeneric_DifferentType_Equal() + { + var expected = new MultiComparable(1); + var actual = 1; + + Assert.Equal(expected, (IComparable)actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void NonGeneric_DifferentType_NotEqual() + { + var expected = new MultiComparable(1); + var actual = 2; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: MultiComparable {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + +#else + "Expected: MultiComparable { Value = 1 }" + Environment.NewLine + +#endif + "Actual: 2", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, (IComparable)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + + [Fact] + public void Generic_Equal() + { + var obj1 = new SpyComparable_Generic(); + var obj2 = new SpyComparable_Generic(); + + Assert.Equal(obj1, obj2); + Assert.True(obj1.CompareCalled); + } + + [Fact] + public void Generic_NotEqual() + { + var obj1 = new SpyComparable_Generic(-1); + var obj2 = new SpyComparable_Generic(); + + var ex = Record.Exception(() => Assert.Equal(obj1, obj2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: SpyComparable_Generic {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: SpyComparable_Generic {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: SpyComparable_Generic { CompareCalled = True }" + Environment.NewLine + + "Actual: SpyComparable_Generic { CompareCalled = False }", +#endif + ex.Message + ); + } + +#if !XUNIT_AOT // The default comparer can't go deep without reflection + + [Fact] + public void SubClass_SubClass_Equal() + { + var expected = new ComparableSubClassA(1); + var actual = new ComparableSubClassB(1); + + Assert.Equal(expected as object, actual); + } + + [Fact] + public void SubClass_SubClass_NotEqual() + { + var expected = new ComparableSubClassA(1); + var actual = new ComparableSubClassB(2); + + var ex = Record.Exception(() => Assert.Equal(expected as object, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: ComparableSubClassA { Value = 1 }" + Environment.NewLine + + "Actual: ComparableSubClassB { Value = 2 }", + ex.Message + ); + } + +#endif // !XUNIT_AOT + + [Fact] + public void BaseClass_SubClass_Equal() + { + var expected = new ComparableBaseClass(1); + var actual = new ComparableSubClassA(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void BaseClass_SubClass_NotEqual() + { + var expected = new ComparableBaseClass(1); + var actual = new ComparableSubClassA(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: ComparableBaseClass {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: ComparableSubClassA {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: ComparableBaseClass { Value = 1 }" + Environment.NewLine + + "Actual: ComparableSubClassA { Value = 2 }", +#endif + ex.Message + ); + } + + [Fact] + public void SubClass_BaseClass_Equal() + { + var expected = new ComparableSubClassA(1); + var actual = new ComparableBaseClass(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void SubClass_BaseClass_NotEqual() + { + var expected = new ComparableSubClassA(1); + var actual = new ComparableBaseClass(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: ComparableSubClassA {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: ComparableBaseClass {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: ComparableSubClassA { Value = 1 }" + Environment.NewLine + + "Actual: ComparableBaseClass { Value = 2 }", +#endif + ex.Message + ); + } + + [Fact] + public void Generic_ThrowsException_Equal() + { + var expected = new ComparableThrower(1); + var actual = new ComparableThrower(1); + + Assert.Equal(expected, actual); + Assert.Equal(expected, (IComparable)actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void Generic_ThrowsException_NotEqual() + { + var expected = new ComparableThrower(1); + var actual = new ComparableThrower(2); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: ComparableThrower {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: ComparableThrower {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: ComparableThrower { Value = 1 }" + Environment.NewLine + + "Actual: ComparableThrower { Value = 2 }", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (IComparable)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + +#if !XUNIT_AOT // IComparable vs. IComparable cannot be done in Native AOT because of the reflection restrictions + + [Fact] + public void DifferentTypes_ImplicitImplementation_Equal() + { + object expected = new ImplicitIComparableExpected(1); + object actual = new IntWrapper(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void DifferentTypes_ImplicitImplementation_NotEqual() + { + object expected = new ImplicitIComparableExpected(1); + object actual = new IntWrapper(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: ImplicitIComparableExpected { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 2 }", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_ExplicitImplementation_Equal() + { + object expected = new ExplicitIComparableActual(1); + object actual = new IntWrapper(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void DifferentTypes_ExplicitImplementation_NotEqual() + { + object expected = new ExplicitIComparableActual(1); + object actual = new IntWrapper(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: ExplicitIComparableActual { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 2 }", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_Throws_Equal() + { + object expected = new IComparableActualThrower(1); + object actual = new IntWrapper(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void DifferentTypes_Throws_NotEqual() + { + object expected = new IComparableActualThrower(1); + object actual = new IntWrapper(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: IComparableActualThrower { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 2 }", + ex.Message + ); + } + +#endif // !XUNIT_AOT + } + + public class NotComparable + { + [Fact] + public void Equal() + { + var nco1 = new NonComparableObject(); + var nco2 = new NonComparableObject(); + + Assert.Equal(nco1, nco2); + } + + [Fact] + public void NotEqual() + { + var nco1 = new NonComparableObject(false); + var nco2 = new NonComparableObject(); + + var ex = Record.Exception(() => Assert.Equal(nco1, nco2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: NonComparableObject {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: NonComparableObject {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: NonComparableObject { }" + Environment.NewLine + + "Actual: NonComparableObject { }", +#endif + ex.Message + ); + } + } + + public class Equatable + { + [Fact] + public void Equal() + { + var obj1 = new SpyEquatable(); + var obj2 = new SpyEquatable(); + + Assert.Equal(obj1, obj2); + + Assert.True(obj1.Equals__Called); + Assert.Same(obj2, obj1.Equals_Other); + } + + [Fact] + public void NotEqual() + { + var obj1 = new SpyEquatable(false); + var obj2 = new SpyEquatable(); + + var ex = Record.Exception(() => Assert.Equal(obj1, obj2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: SpyEquatable {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: SpyEquatable {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: SpyEquatable { Equals__Called = True, Equals_Other = SpyEquatable { Equals__Called = False, Equals_Other = null } }" + Environment.NewLine + + "Actual: SpyEquatable { Equals__Called = False, Equals_Other = null }", +#endif + ex.Message + ); + } + + [Fact] + public void SubClass_SubClass_Equal() + { + var expected = new EquatableSubClassA(1); + var actual = new EquatableSubClassB(1); + + Assert.Equal(expected as object, actual); + } + + [Fact] + public void SubClass_SubClass_NotEqual() + { + var expected = new EquatableSubClassA(1); + var actual = new EquatableSubClassB(2); + + var ex = Record.Exception(() => Assert.Equal(expected as object, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: EquatableSubClassA {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: EquatableSubClassB {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: EquatableSubClassA { Value = 1 }" + Environment.NewLine + + "Actual: EquatableSubClassB { Value = 2 }", +#endif + ex.Message + ); + } + + [Fact] + public void BaseClass_SubClass_Equal() + { + var expected = new EquatableBaseClass(1); + var actual = new EquatableSubClassA(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void BaseClass_SubClass_NotEqual() + { + var expected = new EquatableBaseClass(1); + var actual = new EquatableSubClassA(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: EquatableBaseClass {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: EquatableSubClassA {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: EquatableBaseClass { Value = 1 }" + Environment.NewLine + + "Actual: EquatableSubClassA { Value = 2 }", +#endif + ex.Message + ); + } + + [Fact] + public void SubClass_BaseClass_Equal() + { + var expected = new EquatableSubClassA(1); + var actual = new EquatableBaseClass(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void SubClass_BaseClass_NotEqual() + { + var expected = new EquatableSubClassA(1); + var actual = new EquatableBaseClass(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: EquatableSubClassA {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: EquatableBaseClass {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: EquatableSubClassA { Value = 1 }" + Environment.NewLine + + "Actual: EquatableBaseClass { Value = 2 }", +#endif + ex.Message + ); + } + +#if !XUNIT_AOT // Support for IEquatable vs. IEquatable cannot be done in Native AOT because of the reflection restrictions + + [Fact] + public void DifferentTypes_ImplicitImplementation_Equal() + { + object expected = new ImplicitIEquatableExpected(1); + object actual = new IntWrapper(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void DifferentTypes_ImplicitImplementation_NotEqual() + { + object expected = new ImplicitIEquatableExpected(1); + object actual = new IntWrapper(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: ImplicitIEquatableExpected { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 2 }", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_ExplicitImplementation_Equal() + { + object expected = new ExplicitIEquatableExpected(1); + object actual = new IntWrapper(1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void DifferentTypes_ExplicitImplementation_NotEqual() + { + object expected = new ExplicitIEquatableExpected(1); + object actual = new IntWrapper(2); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: ExplicitIEquatableExpected { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 2 }", + ex.Message + ); + } + +#endif // !XUNIT_AOT + } + + public class StructuralEquatable + { + [Fact] + public void Equal() + { + var expected = new StructuralStringWrapper("a"); + var actual = new StructuralStringWrapper("a"); + + Assert.Equal(expected, actual); + Assert.Equal(expected, (IStructuralEquatable)actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void NotEqual() + { + var expected = new StructuralStringWrapper("a"); + var actual = new StructuralStringWrapper("b"); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: StructuralStringWrapper {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: StructuralStringWrapper {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: StructuralStringWrapper { Value = \"a\" }" + Environment.NewLine + + "Actual: StructuralStringWrapper { Value = \"b\" }", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (IStructuralEquatable)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + + [Fact] + public void ExpectedNull_ActualNull() + { + var expected = new Tuple(null); + var actual = new Tuple(null); + + Assert.Equal(expected, actual); + Assert.Equal(expected, (IStructuralEquatable)actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void ExpectedNull_ActualNonNull() + { + var expected = new Tuple(null); + var actual = new Tuple(new StringWrapper("a")); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: Tuple (null)" + Environment.NewLine + +#if XUNIT_AOT + $"Actual: Tuple (StringWrapper {{ {ArgumentFormatter.Ellipsis} }})", +#else + "Actual: Tuple (StringWrapper { Value = \"a\" })", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (IStructuralEquatable)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + + [Fact] + public void ExpectedNonNull_ActualNull() + { + var expected = new Tuple(new StringWrapper("a")); + var actual = new Tuple(null); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Tuple (StringWrapper {{ {ArgumentFormatter.Ellipsis} }})" + Environment.NewLine + +#else + "Expected: Tuple (StringWrapper { Value = \"a\" })" + Environment.NewLine + +#endif + "Actual: Tuple (null)", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (IStructuralEquatable)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + } + + public class Collections + { + [Fact] + public void IReadOnlyCollection_IEnumerable_Equal() + { + var expected = new string[] { "foo", "bar" }; + var actual = new ReadOnlyCollection(expected); + + Assert.Equal(expected, (IReadOnlyCollection)actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void IReadOnlyCollection_IEnumerable_NotEqual() + { + var expected = new string[] { @"C:\Program Files (x86)\Common Files\Extremely Long Path Name\VST2" }; + var actual = new ReadOnlyCollection([@"C:\Program Files (x86)\Common Files\Extremely Long Path Name\VST3"]); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( +#if XUNIT_AOT + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: string[] [\"C:\\\\Program Files (x86)\\\\Common Files\\\\Extremely L\"···]" + Environment.NewLine + + "Actual: ReadOnlyCollection [\"C:\\\\Program Files (x86)\\\\Common Files\\\\Extremely L\"···]" + Environment.NewLine + + " ↑ (pos 0)", +#else + "Assert.Equal() Failure: Collections differ at index 0" + Environment.NewLine + + " ↓ (pos 64)" + Environment.NewLine + + "Expected: ···\"s (x86)\\\\Common Files\\\\Extremely Long Path Name\\\\VST2\"" + Environment.NewLine + + "Actual: ···\"s (x86)\\\\Common Files\\\\Extremely Long Path Name\\\\VST3\"" + Environment.NewLine + + " ↑ (pos 64)", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, (IReadOnlyCollection)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + + [Fact] + public void CollectionDepth_Equal() + { + var x = new List { new List { new List { 1 } } }; + var y = new List { new List { new List { 1 } } }; + + Assert.Equal(x, y); + } + + [Fact] + public void CollectionDepth_NotEqual() + { + var x = new List { new List { new List { 1 } } }; + var y = new List { new List { new List { 2 } } }; + + var ex = Record.Exception(() => Assert.Equal(x, y)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 0)" + Environment.NewLine + + "Expected: [[[1]]]" + Environment.NewLine + + "Actual: [[[2]]]" + Environment.NewLine + + " ↑ (pos 0)", + ex.Message + ); + } + + [Fact] + public void StringArray_ObjectArray_Equal() + { + var expected = new string[] { "foo", "bar" }; + var actual = new object[] { "foo", "bar" }; + + Assert.Equal(expected, actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void StringArray_ObjectArray_NotEqual() + { + var expected = new string[] { "foo", "bar" }; + var actual = new object[] { "foo", "baz" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( +#if XUNIT_AOT + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Expected: string[] [\"foo\", \"bar\"]" + Environment.NewLine + + "Actual: object[] [\"foo\", \"baz\"]" + Environment.NewLine + + " ↑ (pos 1)", +#else + "Assert.Equal() Failure: Collections differ at index 1" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + "Expected: \"bar\"" + Environment.NewLine + + "Actual: \"baz\"" + Environment.NewLine + + " ↑ (pos 2)", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + + [Fact] + public void MultidimensionalArrays_Equal() + { + var expected = new int[,] { { 1 }, { 2 } }; + var actual = new int[,] { { 1 }, { 2 } }; + + Assert.Equal(expected, actual); + } + + [Fact] + public void MultidimensionalArrays_NotEqual() + { + var expected = new int[,] { { 1, 2 } }; + var actual = new int[,] { { 1 }, { 2 } }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + // TODO: Would be better to have formatting that preserves the ranks instead of + // flattening, which happens because multi-dimensional arrays enumerate flatly + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: [1, 2]" + Environment.NewLine + + "Actual: [1, 2]", + ex.Message + ); + } + +#if !XUNIT_AOT // Array.CreateInstance is not available in Native AOT + + [Fact] + public void NonZeroBoundedArrays_Equal() + { + var expected = Array.CreateInstance(typeof(int), [1], [1]); + expected.SetValue(42, 1); + var actual = Array.CreateInstance(typeof(int), [1], [1]); + actual.SetValue(42, 1); + + Assert.Equal(expected, actual); + } + + [Fact] + public void NonZeroBoundedArrays_NotEqual() + { + var expected = Array.CreateInstance(typeof(int), [1], [1]); + expected.SetValue(42, 1); + var actual = Array.CreateInstance(typeof(int), [1], [0]); + actual.SetValue(42, 0); + + var ex = Record.Exception(() => Assert.Equal(expected, (object)actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: int[*] [42]" + Environment.NewLine + + "Actual: int[] [42]", + ex.Message + ); + } + +#endif // !XUNIT_AOT + + [Fact] + public void PrintPointersWithCompatibleComparers() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new[] { 1, 2, 0, 4, 5 }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + "Expected: [1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: [1, 2, 0, 4, 5]" + Environment.NewLine + + " ↑ (pos 2)", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, actual, EqualityComparer>.Default)); + } + + [Fact] + public void CustomComparerWithSafeEnumerable() + { + var expected = new[] { 1, 2, 3, 4, 5 }; + var actual = new[] { 1, 2, 0, 4, 5 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual, new MyComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: [1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: [1, 2, 0, 4, 5]", + ex.Message + ); + } + + [Fact] + public void CustomComparerWithUnsafeEnumerable() + { + var ex = Record.Exception(() => Assert.Equal(new UnsafeEnumerable(), new[] { 1, 2, 3 }, new MyComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + $"Expected: UnsafeEnumerable [{ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Actual: int[] [1, 2, 3]", + ex.Message + ); + } + + class UnsafeEnumerable : IEnumerable + { + public IEnumerator GetEnumerator() + { + while (true) + yield return 1; + } + } + + class MyComparer : IEqualityComparer + { + public bool Equals(IEnumerable? x, IEnumerable? y) + => false; + + public int GetHashCode([DisallowNull] IEnumerable obj) => + throw new NotImplementedException(); + } + + [Fact] + public void CollectionWithIEquatable_Equal() + { + var expected = new EnumerableEquatable { 42, 2112 }; + var actual = new EnumerableEquatable { 2112, 42 }; + + Assert.Equal(expected, actual); + } + + [Fact] + public void CollectionWithIEquatable_NotEqual() + { + var expected = new EnumerableEquatable { 42, 2112 }; + var actual = new EnumerableEquatable { 2112, 2600 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + // No pointers because it's relying on IEquatable<> + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: [42, 2112]" + Environment.NewLine + + "Actual: [2112, 2600]", + ex.Message + ); + } + + public sealed class EnumerableEquatable : + IEnumerable, IEquatable> + { + readonly List values = []; + + public void Add(T value) => + values.Add(value); + + public bool Equals(EnumerableEquatable? other) + { + if (other == null) + return false; + + return !values.Except(other.values).Any() && !other.values.Except(values).Any(); + } + + public override bool Equals(object? obj) => + Equals(obj as EnumerableEquatable); + + public IEnumerator GetEnumerator() => + values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + public override int GetHashCode() => + values.GetHashCode(); + } + } + + public class Dictionaries + { + [Fact] + public void SameTypes_Equal() + { + var expected = new Dictionary { ["foo"] = "bar" }; + var actual = new Dictionary { ["foo"] = "bar" }; + + Assert.Equal(expected, actual); + Assert.Equal(expected, (IDictionary)actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void SameTypes_NotEqual() + { + var expected = new Dictionary { ["foo"] = "bar" }; + var actual = new Dictionary { ["foo"] = "baz" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Dictionaries differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: [[foo, bar]]" + Environment.NewLine + + "Actual: [[foo, baz]]", +#else + "Expected: [[\"foo\"] = \"bar\"]" + Environment.NewLine + + "Actual: [[\"foo\"] = \"baz\"]", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (IDictionary)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + + [Fact] + public void DifferentTypes_Equal() + { + var expected = new Dictionary { ["foo"] = "bar" }; + var actual = new ConcurrentDictionary(expected); + + Assert.Equal(expected, (IDictionary)actual); + Assert.Equal(expected, (object)actual); + } + + [Fact] + public void DifferentTypes_NotEqual() + { + var expected = new Dictionary { ["foo"] = "bar" }; + var actual = new ConcurrentDictionary { ["foo"] = "baz" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Dictionary [[foo, bar]]" + Environment.NewLine + + "Actual: ConcurrentDictionary [[foo, baz]]", +#else + "Expected: Dictionary [[\"foo\"] = \"bar\"]" + Environment.NewLine + + "Actual: ConcurrentDictionary [[\"foo\"] = \"baz\"]", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, (IDictionary)actual)); + assertFailure(() => Assert.Equal(expected, (object)actual)); + } + + [Fact] + public void NullValue_Equal() + { + var expected = new Dictionary { { "two", null } }; + var actual = new Dictionary { { "two", null } }; + + Assert.Equal(expected, actual); + } + + [Fact] + public void NullValue_NotEqual() + { + var expected = new Dictionary { { "two", null } }; + var actual = new Dictionary { { "two", 1 } }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Dictionaries differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: [[two, ]]" + Environment.NewLine + + "Actual: [[two, 1]]", +#else + "Expected: [[\"two\"] = null]" + Environment.NewLine + + "Actual: [[\"two\"] = 1]", +#endif + ex.Message + ); + } + } + + public class HashSets + { + [Fact] + public static void InOrder_Equal() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 3 }; + + Assert.Equal(expected, actual); + } + + [Fact] + public static void InOrder_NotEqual() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 4 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: HashSets differ" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 4]", + ex.Message + ); + } + + [Fact] + public static void OutOfOrder_Equal() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 2, 3, 1 }; + + Assert.Equal(expected, actual); + } + + [Fact] + public static void OutOfOrder_NotEqual() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 2, 4, 1 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: HashSets differ" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [2, 4, 1]", + ex.Message + ); + } + + [Fact] + public static void ExpectedLarger() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: HashSets differ" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2]", + ex.Message + ); + } + + [Fact] + public static void ActualLarger() + { + var expected = new HashSet { 1, 2 }; + var actual = new HashSet { 1, 2, 3 }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: HashSets differ" + Environment.NewLine + + "Expected: [1, 2]" + Environment.NewLine + + "Actual: [1, 2, 3]", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_Equal() + { + var expected = new HashSet { "bar", "foo" }; + var actual = new SortedSet { "foo", "bar" }; + + Assert.Equal(expected, actual); + Assert.Equal(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.Equal(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection + + [Fact] + public void DifferentTypes_NotEqual() + { + object expected = new HashSet { 42 }; + object actual = new HashSet { 42L }; + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: HashSets differ" + Environment.NewLine + + "Expected: HashSet [42]" + Environment.NewLine + + "Actual: HashSet [42]", + ex.Message + ); + } + +#endif // !XUNIT_AOT + +#if !XUNIT_AOT // Comparer func overload is disabled in AOT via compiler + + [Fact] + public void ComparerFunc_Throws() + { + var expected = new HashSet { "bar" }; + var actual = new HashSet { "baz" }; + +#pragma warning disable xUnit2026 // Comparison of sets must be done with IEqualityComparer + var ex = Record.Exception(() => Assert.Equal(expected, actual, (string l, string r) => true)); +#pragma warning restore xUnit2026 // Comparison of sets must be done with IEqualityComparer + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: During comparison of two collections, GetHashCode was called, but only a comparison function was provided. This typically indicates trying to compare two sets with an item comparison function, which is not supported. For more information, see https://xunit.net/docs/hash-sets-vs-linear-containers", + ex.Message + ); + } + +#endif // !XUNIT_AOT + } + + public class Sets + { + [Fact] + public void InOrder_Equal() + { + var expected = new NonGenericSet { "bar", "foo" }; + var actual = new NonGenericSet { "bar", "foo" }; + + Assert.Equal(expected, actual); + Assert.Equal(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.Equal(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void InOrder_NotEqual() + { + var expected = new NonGenericSet { "bar", "foo" }; + var actual = new NonGenericSet { "bar", "baz" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Sets differ" + Environment.NewLine + + "Expected: [\"bar\", \"foo\"]" + Environment.NewLine + + "Actual: [\"bar\", \"baz\"]", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.Equal(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void OutOfOrder_Equal() + { + var expected = new NonGenericSet { "bar", "foo" }; + var actual = new NonGenericSet { "foo", "bar" }; + + Assert.Equal(expected, actual); + Assert.Equal(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.Equal(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void OutOfOrder_NotEqual() + { + var expected = new NonGenericSet { "bar", "foo" }; + var actual = new NonGenericSet { "foo", "baz" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Sets differ" + Environment.NewLine + + "Expected: [\"bar\", \"foo\"]" + Environment.NewLine + + "Actual: [\"foo\", \"baz\"]", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.Equal(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void DifferentContents() + { + var expected = new NonGenericSet { "bar" }; + var actual = new NonGenericSet { "bar", "foo" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Sets differ" + Environment.NewLine + + "Expected: [\"bar\"]" + Environment.NewLine + + "Actual: [\"bar\", \"foo\"]", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.Equal(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void DifferentTypes_Equal() + { + var expected = new NonGenericSet { "bar" }; + var actual = new HashSet { "bar" }; + + Assert.Equal(expected, actual); + Assert.Equal(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.Equal(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void DifferentTypes_NotEqual() + { + var expected = new NonGenericSet { "bar" }; + var actual = new HashSet { "baz" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Sets differ" + Environment.NewLine + + "Expected: NonGenericSet [\"bar\"]" + Environment.NewLine + + "Actual: HashSet [\"baz\"]", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.Equal(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void TwoGenericSubClass_Equal() + { + var expected = new TwoGenericSet { "foo", "bar" }; + var actual = new TwoGenericSet { "foo", "bar" }; + + Assert.Equal(expected, actual); + Assert.Equal(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.Equal(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void TwoGenericSubClass_NotEqual() + { + var expected = new TwoGenericSet { "foo", "bar" }; + var actual = new TwoGenericSet { "foo", "baz" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Sets differ" + Environment.NewLine + + "Expected: [\"foo\", \"bar\"]" + Environment.NewLine + + "Actual: [\"foo\", \"baz\"]", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected, actual)); + assertFailure(() => Assert.Equal(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.Equal(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + +#if !XUNIT_AOT // Comparer func overload is disabled in AOT via compiler + + [Fact] + public void ComparerFunc_Throws() + { + var expected = new NonGenericSet { "bar" }; + var actual = new HashSet { "baz" }; + +#pragma warning disable xUnit2026 // Comparison of sets must be done with IEqualityComparer + var ex = Record.Exception(() => Assert.Equal(expected, actual, (string l, string r) => true)); +#pragma warning restore xUnit2026 // Comparison of sets must be done with IEqualityComparer + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: During comparison of two collections, GetHashCode was called, but only a comparison function was provided. This typically indicates trying to compare two sets with an item comparison function, which is not supported. For more information, see https://xunit.net/docs/hash-sets-vs-linear-containers", + ex.Message + ); + } + +#endif + } + + // https://github.com/xunit/xunit/issues/3137 + public class ImmutableArrays + { + [Fact] + public void Equal() + { + var expected = new[] { 1, 2, 3 }.ToImmutableArray(); + var actual = new[] { 1, 2, 3 }.ToImmutableArray(); + + Assert.Equal(expected, actual); + } + + [Fact] + public void NotEqual() + { + var expected = new[] { 1, 2, 3 }.ToImmutableArray(); + var actual = new[] { 1, 2, 4 }.ToImmutableArray(); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 4]" + Environment.NewLine + + " ↑ (pos 2)", + ex.Message + ); + } + } + + public class KeyValuePair + { +#if !XUNIT_AOT // AOT can't deep dive into collections in the same was a non-AOT + + [Fact] + public void CollectionKeys_Equal() + { +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + // Different concrete collection types in the key slot, per https://github.com/xunit/xunit/issues/2850 + var expected = new KeyValuePair, int>(new List { "Key1", "Key2" }, 42); + var actual = new KeyValuePair, int>(new string[] { "Key1", "Key2" }, 42); +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + Assert.Equal(expected, actual); + } + + [Fact] + public void CollectionKeys_NotEqual() + { +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + // Different concrete collection types in the key slot, per https://github.com/xunit/xunit/issues/2850 + var expected = new KeyValuePair, int>(new List { "Key1", "Key2" }, 42); + var actual = new KeyValuePair, int>(new string[] { "Key1", "Key3" }, 42); +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: [[\"Key1\", \"Key2\"]] = 42" + Environment.NewLine + + "Actual: [[\"Key1\", \"Key3\"]] = 42", + ex.Message + ); + } + + [Fact] + public void CollectionValues_Equal() + { +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + // Different concrete collection types in the value slot, per https://github.com/xunit/xunit/issues/2850 + var expected = new KeyValuePair>("Key1", new List { "Value1a", "Value1b" }); + var actual = new KeyValuePair>("Key1", new string[] { "Value1a", "Value1b" }); +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + Assert.Equal(expected, actual); + } + + [Fact] + public void CollectionValues_NotEqual() + { +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + // Different concrete collection types in the value slot, per https://github.com/xunit/xunit/issues/2850 + var expected = new KeyValuePair>("Key1", new List { "Value1a", "Value1b" }); + var actual = new KeyValuePair>("Key1", new string[] { "Value1a", "Value2a" }); +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + "Expected: [\"Key1\"] = [\"Value1a\", \"Value1b\"]" + Environment.NewLine + + "Actual: [\"Key1\"] = [\"Value1a\", \"Value2a\"]", + ex.Message + ); + } + +#endif // !XUNIT_AOT + + [Fact] + public void EquatableKeys_Equal() + { + var expected = new KeyValuePair(new() { Char = 'a' }, 42); + var actual = new KeyValuePair(new() { Char = 'a' }, 42); + + Assert.Equal(expected, actual); + } + + [Fact] + public void EquatableKeys_NotEqual() + { + var expected = new KeyValuePair(new() { Char = 'a' }, 42); + var actual = new KeyValuePair(new() { Char = 'b' }, 42); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: [EqualityAssertsTests+Equal+KeyValuePair+EquatableObject, 42]" + Environment.NewLine + + "Actual: [EqualityAssertsTests+Equal+KeyValuePair+EquatableObject, 42]", +#else + "Expected: [EquatableObject { Char = 'a' }] = 42" + Environment.NewLine + + "Actual: [EquatableObject { Char = 'b' }] = 42", +#endif + ex.Message + ); + } + + [Fact] + public void EquatableValues_Equal() + { + var expected = new KeyValuePair("Key1", new() { Char = 'a' }); + var actual = new KeyValuePair("Key1", new() { Char = 'a' }); + + Assert.Equal(expected, actual); + } + + [Fact] + public void EquatableValues_NotEqual() + { + var expected = new KeyValuePair("Key1", new() { Char = 'a' }); + var actual = new KeyValuePair("Key1", new() { Char = 'b' }); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + +#if XUNIT_AOT + "Expected: [Key1, EqualityAssertsTests+Equal+KeyValuePair+EquatableObject]" + Environment.NewLine + + "Actual: [Key1, EqualityAssertsTests+Equal+KeyValuePair+EquatableObject]", +#else + "Expected: [\"Key1\"] = EquatableObject { Char = 'a' }" + Environment.NewLine + + "Actual: [\"Key1\"] = EquatableObject { Char = 'b' }", +#endif + ex.Message + ); + } + + public class EquatableObject : IEquatable + { + public char Char { get; set; } + + public bool Equals(EquatableObject? other) => + other != null && other.Char == Char; + + public override bool Equals(object? obj) => + Equals(obj as EquatableObject); + + public override int GetHashCode() => + Char.GetHashCode(); + } + } + + public class DoubleEnumerationPrevention + { + [Fact] + public static void EnumeratesOnlyOnce_Equal() + { + var expected = new RunOnceEnumerable([1, 2, 3, 4, 5]); + var actual = new RunOnceEnumerable([1, 2, 3, 4, 5]); + + Assert.Equal(expected, actual); + } + + [Fact] + public static void EnumeratesOnlyOnce_NotEqual() + { + var expected = new RunOnceEnumerable([1, 2, 3, 4, 5]); + var actual = new RunOnceEnumerable([1, 2, 3, 4, 5, 6]); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + $"Expected: [{ArgumentFormatter.Ellipsis}, 2, 3, 4, 5]" + Environment.NewLine + + $"Actual: [{ArgumentFormatter.Ellipsis}, 2, 3, 4, 5, 6]" + Environment.NewLine + + " ↑ (pos 5)", + ex.Message + ); + } + } + } + + public class Equal_DateTime + { + public class WithoutPrecision + { + [Fact] + public void Equal() + { + var expected = new DateTime(2023, 2, 11, 15, 4, 0); + var actual = new DateTime(2023, 2, 11, 15, 4, 0); + + Assert.Equal(expected, actual); + } + + [CulturedFactDefault] + public void NotEqual() + { + var expected = new DateTime(2023, 2, 11, 15, 4, 0); + var actual = new DateTime(2023, 2, 11, 15, 5, 0); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(expected)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(actual)}", + ex.Message + ); + } + } + + public class WithPrecision + { + [Fact] + public void InRange() + { + var date1 = new DateTime(2023, 2, 11, 15, 4, 0); + var date2 = new DateTime(2023, 2, 11, 15, 5, 0); + var precision = TimeSpan.FromMinutes(1); + + Assert.Equal(date1, date2, precision); // expected earlier than actual + Assert.Equal(date2, date1, precision); // expected later than actual + } + + [CulturedFactDefault] + public void OutOfRange() + { + var date1 = new DateTime(2023, 2, 11, 15, 4, 0); + var date2 = new DateTime(2023, 2, 11, 15, 6, 0); + var precision = TimeSpan.FromMinutes(1); + var difference = TimeSpan.FromMinutes(2); + + // expected earlier than actual + var ex = Record.Exception(() => Assert.Equal(date1, date2, precision)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(date1)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(date2)} (difference {difference} is larger than {precision})", + ex.Message + ); + + // expected later than actual + var ex2 = Record.Exception(() => Assert.Equal(date2, date1, precision)); + + Assert.IsType(ex2); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(date2)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(date1)} (difference {difference} is larger than {precision})", + ex2.Message + ); + } + } + } + + public class Equal_DateTimeOffset + { + public class WithoutPrecision_SameTimeZone + { + [Fact] + public void Equal() + { + var expected = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + var actual = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + + Assert.Equal(expected, actual); + Assert.Equal(expected, actual); + } + + [CulturedFactDefault] + public void NotEqual() + { + var expected = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + var actual = new DateTimeOffset(2023, 2, 11, 15, 5, 0, TimeSpan.Zero); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(expected)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(actual)}", + ex.Message + ); + } + } + + public class WithoutPrecision_DifferentTimeZone + { + [Fact] + public void Equal() + { + var expected = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + var actual = new DateTimeOffset(2023, 2, 11, 16, 4, 0, TimeSpan.FromHours(1)); + + Assert.Equal(expected, actual); + } + + [CulturedFactDefault] + public void NotEqual() + { + var expected = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + var actual = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.FromHours(1)); + + var ex = Record.Exception(() => Assert.Equal(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(expected)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(actual)}", + ex.Message + ); + } + } + + public class WithPrecision_SameTimeZone + { + [Fact] + public void InRange() + { + var date1 = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + var date2 = new DateTimeOffset(2023, 2, 11, 15, 5, 0, TimeSpan.Zero); + var precision = TimeSpan.FromMinutes(1); + + Assert.Equal(date1, date2, precision); // expected earlier than actual + Assert.Equal(date2, date1, precision); // expected later than actual + } + + [CulturedFactDefault] + public void OutOfRange() + { + var date1 = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + var date2 = new DateTimeOffset(2023, 2, 11, 15, 6, 0, TimeSpan.Zero); + var precision = TimeSpan.FromMinutes(1); + var difference = TimeSpan.FromMinutes(2); + + // expected earlier than actual + var ex = Record.Exception(() => Assert.Equal(date1, date2, precision)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(date1)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(date2)} (difference {difference} is larger than {precision})", + ex.Message + ); + + // expected later than actual + var ex2 = Record.Exception(() => Assert.Equal(date2, date1, precision)); + + Assert.IsType(ex2); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(date2)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(date1)} (difference {difference} is larger than {precision})", + ex2.Message + ); + } + } + + public class WithPrecision_DifferentTimeZone + { + [Fact] + public void InRange() + { + var date1 = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + var date2 = new DateTimeOffset(2023, 2, 11, 16, 5, 0, TimeSpan.FromHours(1)); + var precision = TimeSpan.FromMinutes(1); + + Assert.Equal(date1, date2, precision); // expected earlier than actual + Assert.Equal(date2, date1, precision); // expected later than actual + } + + [CulturedFactDefault] + public void OutOfRange() + { + var date1 = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.Zero); + var date2 = new DateTimeOffset(2023, 2, 11, 15, 4, 0, TimeSpan.FromHours(1)); + var precision = TimeSpan.FromMinutes(1); + var difference = TimeSpan.FromHours(1); + + // expected earlier than actual + var ex = Record.Exception(() => Assert.Equal(date1, date2, precision)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(date1)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(date2)} (difference {difference} is larger than {precision})", + ex.Message + ); + + // expected later than actual + var ex2 = Record.Exception(() => Assert.Equal(date2, date1, precision)); + + Assert.IsType(ex2); + Assert.Equal( + $"Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Format(date2)}" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Format(date1)} (difference {difference} is larger than {precision})", + ex2.Message + ); + } + } + } + + public class Equal_Decimal + { + [Fact] + public void Equal() + { + Assert.Equal(0.11111M, 0.11444M, 2); + } + + [CulturedFactDefault] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.11111M, 0.11444M, 3)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values differ" + Environment.NewLine + + $"Expected: {0.111M} (rounded from {0.11111M})" + Environment.NewLine + + $"Actual: {0.114M} (rounded from {0.11444M})", + ex.Message + ); + } + } + + public class Equal_Double + { + public class WithPrecision + { + [Fact] + public void Equal() + { + Assert.Equal(0.11111, 0.11444, 2); + } + + [CulturedFactDefault] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.11111, 0.11444, 3)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values are not within 3 decimal places" + Environment.NewLine + + $"Expected: {0.111:G17} (rounded from {0.11111:G17})" + Environment.NewLine + + $"Actual: {0.114:G17} (rounded from {0.11444:G17})", + ex.Message + ); + } + } + + public class WithMidPointRounding + { + [Fact] + public void Equal() + { + Assert.Equal(10.565, 10.566, 2, MidpointRounding.AwayFromZero); + } + + [CulturedFactDefault] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.11113, 0.11115, 4, MidpointRounding.ToEven)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within 4 decimal places" + Environment.NewLine + + $"Expected: {0.1111:G17} (rounded from {0.11113:G17})" + Environment.NewLine + + $"Actual: {0.1112:G17} (rounded from {0.11115:G17})", + ex.Message + ); + } + } + + public class WithTolerance + { + [Fact] + public void GuardClause() + { + var ex = Record.Exception(() => Assert.Equal(0.0, 1.0, double.NegativeInfinity)); + + var argEx = Assert.IsType(ex); + Assert.StartsWith("Tolerance must be greater than or equal to zero", ex.Message); + Assert.Equal("tolerance", argEx.ParamName); + } + + [Fact] + public void Equal() + { + Assert.Equal(10.566, 10.565, 0.01); + } + + [CulturedFactDefault] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.11113, 0.11115, 0.00001)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within tolerance {0.00001:G17}" + Environment.NewLine + + $"Expected: {0.11113:G17}" + Environment.NewLine + + $"Actual: {0.11115:G17}", + ex.Message + ); + } + + [Fact] + public void NaN_Equal() + { + Assert.Equal(double.NaN, double.NaN, 1000.0); + } + + [CulturedFactDefault] + public void NaN_NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(20210102.2208, double.NaN, 20000000.0)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within tolerance {20000000.0:G17}" + Environment.NewLine + + $"Expected: {20210102.2208:G17}" + Environment.NewLine + + $"Actual: NaN", + ex.Message + ); + } + + [Fact] + public void InfiniteTolerance_Equal() + { + Assert.Equal(double.MinValue, double.MaxValue, double.PositiveInfinity); + } + + [CulturedFactDefault] + public void PositiveInfinity_NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(double.PositiveInfinity, 77.7, 1.0)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within tolerance {1.0:G17}" + Environment.NewLine + + $"Expected: {double.PositiveInfinity}" + Environment.NewLine + + $"Actual: {77.7:G17}", + ex.Message + ); + } + + [CulturedFactDefault] + public void NegativeInfinity_NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.0, double.NegativeInfinity, 1.0)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within tolerance {1.0:G17}" + Environment.NewLine + + $"Expected: {0.0:G17}" + Environment.NewLine + + $"Actual: {double.NegativeInfinity}", + ex.Message + ); + } + } + } + + public class Equal_Float + { + public class WithPrecision + { + [Fact] + public void Equal() + { + Assert.Equal(0.11111f, 0.11444f, 2); + } + + [CulturedFactDefault] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.11111f, 0.11444f, 3)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values are not within 3 decimal places" + Environment.NewLine + + $"Expected: {0.111:G9} (rounded from {0.11111f:G9})" + Environment.NewLine + + $"Actual: {0.114:G9} (rounded from {0.11444f:G9})", + ex.Message + ); + } + } + + public class WithMidPointRounding + { + [Fact] + public void Equal() + { + Assert.Equal(10.5655f, 10.5666f, 2, MidpointRounding.AwayFromZero); + } + + [CulturedFactDefault] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.111133f, 0.111155f, 4, MidpointRounding.ToEven)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Values are not within 4 decimal places" + Environment.NewLine + + $"Expected: {0.1111:G9} (rounded from {0.111133f:G9})" + Environment.NewLine + + $"Actual: {0.1112:G9} (rounded from {0.111155f:G9})", + ex.Message + ); + } + } + + public class WithTolerance + { + [Fact] + public void GuardClause() + { + var ex = Record.Exception(() => Assert.Equal(0.0f, 1.0f, float.NegativeInfinity)); + + var argEx = Assert.IsType(ex); + Assert.StartsWith("Tolerance must be greater than or equal to zero", ex.Message); + Assert.Equal("tolerance", argEx.ParamName); + } + + [Fact] + public void Equal() + { + Assert.Equal(10.569f, 10.562f, 0.01f); + } + + [CulturedFactDefault] + public void NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.11113f, 0.11115f, 0.00001f)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within tolerance {0.00001f:G9}" + Environment.NewLine + + $"Expected: {0.11113f:G9}" + Environment.NewLine + + $"Actual: {0.11115f:G9}", + ex.Message + ); + } + + [Fact] + public void NaN_Equal() + { + Assert.Equal(float.NaN, float.NaN, 1000.0f); + } + + [CulturedFactDefault] + public void NaN_NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(20210102.2208f, float.NaN, 20000000.0f)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within tolerance {20000000.0f:G9}" + Environment.NewLine + + $"Expected: {20210102.2208f:G9}" + Environment.NewLine + + $"Actual: NaN", + ex.Message + ); + } + + [Fact] + public void InfiniteTolerance_Equal() + { + Assert.Equal(float.MinValue, float.MaxValue, float.PositiveInfinity); + } + + [CulturedFactDefault] + public void PositiveInfinity_NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(float.PositiveInfinity, 77.7f, 1.0f)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within tolerance {1.0f:G9}" + Environment.NewLine + + $"Expected: {float.PositiveInfinity}" + Environment.NewLine + + $"Actual: {77.7f:G9}", + ex.Message + ); + } + + [CulturedFactDefault] + public void NegativeInfinity_NotEqual() + { + var ex = Record.Exception(() => Assert.Equal(0.0f, float.NegativeInfinity, 1.0f)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equal() Failure: Values are not within tolerance {1.0f:G9}" + Environment.NewLine + + $"Expected: {0.0f:G9}" + Environment.NewLine + + $"Actual: {float.NegativeInfinity}", + ex.Message + ); + } + } + } + + public class NotEqual + { + public class Intrinsics + { + [Fact] + public void EqualValues() + { + var ex = Record.Exception(() => Assert.NotEqual(42, 42)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not 42" + Environment.NewLine + + "Actual: 42", + ex.Message + ); + } + + [Fact] + public void UnequalValues() + { + Assert.NotEqual(42, 2112); + } + } + + public class WithComparer + { + [Fact] + public void GuardClause() + { + Assert.Throws("comparer", () => Assert.NotEqual(1, 2, default(IEqualityComparer)!)); + } + + [Fact] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(42, 21, new Comparer(true))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not 42" + Environment.NewLine + + "Actual: 21", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(42, 42, new Comparer(false)); + } + + class Comparer(bool result) : + IEqualityComparer + { + readonly bool result = result; + + public bool Equals(T? x, T? y) => result; + + public int GetHashCode(T obj) => throw new NotImplementedException(); + } + + [Fact] + public void NonEnumerable_WithThrow_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.NotEqual(42, 42, new ThrowingIntComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: Not 42" + Environment.NewLine + + "Actual: 42", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + public class ThrowingIntComparer : IEqualityComparer + { + public bool Equals(int x, int y) => + throw new DivideByZeroException(); + public int GetHashCode(int obj) => + throw new NotImplementedException(); + } + + [Fact] + public void Enumerable_WithThrow_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.NotEqual([1, 2], [1, 2], new ThrowingEnumerableComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: Not [1, 2]" + Environment.NewLine + + "Actual: [1, 2]", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + public class ThrowingEnumerableComparer : IEqualityComparer> + { + public bool Equals(IEnumerable? x, IEnumerable? y) => + throw new DivideByZeroException(); + public int GetHashCode(IEnumerable obj) => + throw new NotImplementedException(); + } + + [Fact] + public void Strings_WithThrow_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.NotEqual("42", "42", new ThrowingStringComparer())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: Not \"42\"" + Environment.NewLine + + "Actual: \"42\"", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + public class ThrowingStringComparer : IEqualityComparer + { + public bool Equals(string? x, string? y) => + throw new DivideByZeroException(); + public int GetHashCode(string obj) => + throw new NotImplementedException(); + } + } + + public class WithFunc + { + [Fact] + public void GuardClause() + { + Assert.Throws("comparer", () => Assert.NotEqual(1, 2, default(Func)!)); + } + + [Fact] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(42, 21, (x, y) => true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not 42" + Environment.NewLine + + "Actual: 21", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(42, 42, (x, y) => false); + } + + [Fact] + public void NonEnumerable_WithThrow_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.NotEqual(42, 42, (e, a) => throw new DivideByZeroException())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: Not 42" + Environment.NewLine + + "Actual: 42", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + [Fact] + public void Enumerable_WithThrow_RecordsInnerException() + { + var ex = Record.Exception( + () => Assert.NotEqual( + [1, 2], + [1, 2], + (IEnumerable e, IEnumerable a) => throw new DivideByZeroException() + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: Not [1, 2]" + Environment.NewLine + + "Actual: [1, 2]", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + + [Fact] + public void Strings_WithThrow_RecordsInnerException() + { + var ex = Record.Exception(() => Assert.NotEqual("42", "42", (e, a) => throw new DivideByZeroException())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine + + "Expected: Not \"42\"" + Environment.NewLine + + "Actual: \"42\"", + ex.Message + ); + Assert.IsType(ex.InnerException); + } + } + + public class Comparable + { + [Fact] + public void Equal() + { + var obj1 = new SpyComparable(0); + var obj2 = new SpyComparable(0); + + var ex = Record.Exception(() => Assert.NotEqual(obj1, obj2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not SpyComparable {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: SpyComparable {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not SpyComparable { CompareCalled = True }" + Environment.NewLine + + "Actual: SpyComparable { CompareCalled = False }", +#endif + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + var obj1 = new SpyComparable(-1); + var obj2 = new SpyComparable(0); + + Assert.NotEqual(obj1, obj2); + Assert.True(obj1.CompareCalled); + } + + [Fact] + public void NonGeneric_SameType_Equal() + { + var expected = new MultiComparable(1); + var actual = new MultiComparable(1); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not MultiComparable {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: MultiComparable {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not MultiComparable { Value = 1 }" + Environment.NewLine + + "Actual: MultiComparable { Value = 1 }", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (IComparable)actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void NonGeneric_SameType_NotEqual() + { + var expected = new MultiComparable(1); + var actual = new MultiComparable(2); + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (IComparable)actual); + Assert.NotEqual(expected, (object)actual); + } + + [Fact] + public void NonGeneric_DifferentType_Equal() + { + var expected = new MultiComparable(1); + var actual = 1; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not MultiComparable {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + +#else + "Expected: Not MultiComparable { Value = 1 }" + Environment.NewLine + +#endif + "Actual: 1", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, (IComparable)actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void NonGeneric_DifferentType_NotEqual() + { + var expected = new MultiComparable(1); + var actual = 2; + + Assert.NotEqual(expected, (IComparable)actual); + Assert.NotEqual(expected, (object)actual); + } + + [Fact] + public void Generic_Equal() + { + var obj1 = new SpyComparable_Generic(); + var obj2 = new SpyComparable_Generic(); + + var ex = Record.Exception(() => Assert.NotEqual(obj1, obj2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not SpyComparable_Generic {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: SpyComparable_Generic {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not SpyComparable_Generic { CompareCalled = True }" + Environment.NewLine + + "Actual: SpyComparable_Generic { CompareCalled = False }", +#endif + ex.Message + ); + } + + [Fact] + public void Generic_NotEqual() + { + var obj1 = new SpyComparable_Generic(-1); + var obj2 = new SpyComparable_Generic(); + + Assert.NotEqual(obj1, obj2); + Assert.True(obj1.CompareCalled); + } + + [Fact] + public void SubClass_SubClass_Equal() + { + var expected = new ComparableSubClassA(1); + var actual = new ComparableSubClassB(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not ComparableSubClassA {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: ComparableSubClassB {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not ComparableSubClassA { Value = 1 }" + Environment.NewLine + + "Actual: ComparableSubClassB { Value = 1 }", +#endif + ex.Message + ); + } + + [Fact] + public void SubClass_SubClass_NotEqual() + { + var expected = new ComparableSubClassA(1); + var actual = new ComparableSubClassB(2); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void BaseClass_SubClass_Equal() + { + var expected = new ComparableBaseClass(1); + var actual = new ComparableSubClassA(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not ComparableBaseClass {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: ComparableSubClassA {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not ComparableBaseClass { Value = 1 }" + Environment.NewLine + + "Actual: ComparableSubClassA { Value = 1 }", +#endif + ex.Message + ); + } + + [Fact] + public void BaseClass_SubClass_NotEqual() + { + var expected = new ComparableBaseClass(1); + var actual = new ComparableSubClassA(2); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void SubClass_BaseClass_Equal() + { + var expected = new ComparableSubClassA(1); + var actual = new ComparableBaseClass(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not ComparableSubClassA {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: ComparableBaseClass {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not ComparableSubClassA { Value = 1 }" + Environment.NewLine + + "Actual: ComparableBaseClass { Value = 1 }", +#endif + ex.Message + ); + } + + [Fact] + public void SubClass_BaseClass_NotEqual() + { + var expected = new ComparableSubClassA(1); + var actual = new ComparableBaseClass(2); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void Generic_ThrowsException_Equal() + { + var expected = new ComparableThrower(1); + var actual = new ComparableThrower(1); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not ComparableThrower {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: ComparableThrower {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not ComparableThrower { Value = 1 }" + Environment.NewLine + + "Actual: ComparableThrower { Value = 1 }", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (IComparable)actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void Generic_ThrowsException_NotEqual() + { + var expected = new ComparableThrower(1); + var actual = new ComparableThrower(2); + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (IComparable)actual); + Assert.NotEqual(expected, (object)actual); + } + +#if !XUNIT_AOT // IComparable vs. IComparable cannot be done in Native AOT because of the reflection restrictions + + [Fact] + public void DifferentTypes_ImplicitImplementation_Equal() + { + object expected = new ImplicitIComparableExpected(1); + object actual = new IntWrapper(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not ImplicitIComparableExpected { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 1 }", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_ImplicitImplementation_NotEqual() + { + object expected = new ImplicitIComparableExpected(1); + object actual = new IntWrapper(2); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void DifferentTypes_ExplicitImplementation_Equal() + { + object expected = new ExplicitIComparableActual(1); + object actual = new IntWrapper(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not ExplicitIComparableActual { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 1 }", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_ExplicitImplementation_NotEqual() + { + object expected = new ExplicitIComparableActual(1); + object actual = new IntWrapper(2); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void DifferentTypes_Throws_Equal() + { + object expected = new IComparableActualThrower(1); + object actual = new IntWrapper(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not IComparableActualThrower { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 1 }", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_Throws_NotEqual() + { + object expected = new IComparableActualThrower(1); + object actual = new IntWrapper(2); + + Assert.NotEqual(expected, actual); + } + +#endif // !XUNIT_AOT + } + + public class NotComparable + { + [Fact] + public void Equal() + { + var nco1 = new NonComparableObject(); + var nco2 = new NonComparableObject(); + + var ex = Record.Exception(() => Assert.NotEqual(nco1, nco2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not NonComparableObject {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: NonComparableObject {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not NonComparableObject { }" + Environment.NewLine + + "Actual: NonComparableObject { }", +#endif + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + var nco1 = new NonComparableObject(false); + var nco2 = new NonComparableObject(); + + Assert.NotEqual(nco1, nco2); + } + } + + public class Equatable + { + [Fact] + public void Equal() + { + var obj1 = new SpyEquatable(); + var obj2 = new SpyEquatable(); + + var ex = Record.Exception(() => Assert.NotEqual(obj1, obj2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not SpyEquatable {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: SpyEquatable {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not SpyEquatable { Equals__Called = True, Equals_Other = SpyEquatable { Equals__Called = False, Equals_Other = null } }" + Environment.NewLine + + "Actual: SpyEquatable { Equals__Called = False, Equals_Other = null }", +#endif + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + var obj1 = new SpyEquatable(false); + var obj2 = new SpyEquatable(); + + Assert.NotEqual(obj1, obj2); + + Assert.True(obj1.Equals__Called); + Assert.Same(obj2, obj1.Equals_Other); + } + + [Fact] + public void SubClass_SubClass_Equal() + { + var expected = new EquatableSubClassA(1); + var actual = new EquatableSubClassB(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not EquatableSubClassA {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: EquatableSubClassB {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not EquatableSubClassA { Value = 1 }" + Environment.NewLine + + "Actual: EquatableSubClassB { Value = 1 }", +#endif + ex.Message + ); + } + + [Fact] + public void SubClass_SubClass_NotEqual() + { + var expected = new EquatableSubClassA(1); + var actual = new EquatableSubClassB(2); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void BaseClass_SubClass_Equal() + { + var expected = new EquatableBaseClass(1); + var actual = new EquatableSubClassA(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not EquatableBaseClass {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: EquatableSubClassA {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not EquatableBaseClass { Value = 1 }" + Environment.NewLine + + "Actual: EquatableSubClassA { Value = 1 }", +#endif + ex.Message + ); + } + + [Fact] + public void BaseClass_SubClass_NotEqual() + { + var expected = new EquatableBaseClass(1); + var actual = new EquatableSubClassA(2); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void SubClass_BaseClass_Equal() + { + var expected = new EquatableSubClassA(1); + var actual = new EquatableBaseClass(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not EquatableSubClassA {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: EquatableBaseClass {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not EquatableSubClassA { Value = 1 }" + Environment.NewLine + + "Actual: EquatableBaseClass { Value = 1 }", +#endif + ex.Message + ); + } + + [Fact] + public void SubClass_BaseClass_NotEqual() + { + var expected = new EquatableSubClassA(1); + var actual = new EquatableBaseClass(2); + + Assert.NotEqual(expected, actual); + } + +#if !XUNIT_AOT // Support for IEquatable vs. IEquatable cannot be done in Native AOT because of the reflection restrictions + + [Fact] + public void DifferentTypes_ImplicitImplementation_Equal() + { + object expected = new ImplicitIEquatableExpected(1); + object actual = new IntWrapper(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not ImplicitIEquatableExpected { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 1 }", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_ImplicitImplementation_NotEqual() + { + object expected = new ImplicitIEquatableExpected(1); + object actual = new IntWrapper(2); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void DifferentTypes_ExplicitImplementation_Equal() + { + object expected = new ExplicitIEquatableExpected(1); + object actual = new IntWrapper(1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not ExplicitIEquatableExpected { Value = 1 }" + Environment.NewLine + + "Actual: IntWrapper { Value = 1 }", + ex.Message + ); + } + + [Fact] + public void DifferentTypes_ExplicitImplementation_NotEqual() + { + object expected = new ExplicitIEquatableExpected(1); + object actual = new IntWrapper(2); + + Assert.NotEqual(expected, actual); + } + +#endif // !XUNIT_AOT + } + + public class StructuralEquatable + { + [Fact] + public void Equal() + { + var expected = new StructuralStringWrapper("a"); + var actual = new StructuralStringWrapper("a"); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not StructuralStringWrapper {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: StructuralStringWrapper {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not StructuralStringWrapper { Value = \"a\" }" + Environment.NewLine + + "Actual: StructuralStringWrapper { Value = \"a\" }", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (IStructuralEquatable)actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void NotEqual() + { + var expected = new StructuralStringWrapper("a"); + var actual = new StructuralStringWrapper("b"); + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (IStructuralEquatable)actual); + Assert.NotEqual(expected, (object)actual); + } + + [Fact] + public void ExpectedNull_ActualNull() + { + var expected = new Tuple(null); + var actual = new Tuple(null); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + "Expected: Not Tuple (null)" + Environment.NewLine + + "Actual: Tuple (null)", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (IStructuralEquatable)actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void ExpectedNull_ActualNonNull() + { + var expected = new Tuple(null); + var actual = new Tuple(new StringWrapper("a")); + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (IStructuralEquatable)actual); + Assert.NotEqual(expected, (object)actual); + } + + [Fact] + public void ExpectedNonNull_ActualNull() + { + var expected = new Tuple(new StringWrapper("a")); + var actual = new Tuple(null); + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (IStructuralEquatable)actual); + Assert.NotEqual(expected, (object)actual); + } + } + + public class Collections + { + [Fact] + public void IReadOnlyCollection_IEnumerable_Equal() + { + var expected = new string[] { "foo", "bar" }; + var actual = new ReadOnlyCollection(expected); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not string[] [\"foo\", \"bar\"]" + Environment.NewLine + + "Actual: ReadOnlyCollection [\"foo\", \"bar\"]", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, (IReadOnlyCollection)actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void IReadOnlyCollection_IEnumerable_NotEqual() + { + var expected = new string[] { "foo", "bar" }; + var actual = new ReadOnlyCollection(["bar", "foo"]); + + Assert.NotEqual(expected, (IReadOnlyCollection)actual); + Assert.NotEqual(expected, (object)actual); + } + + [Fact] + public void CollectionDepth_Equal() + { + var x = new List { new List { new List { 1 } } }; + var y = new List { new List { new List { 1 } } }; + + var ex = Record.Exception(() => Assert.NotEqual(x, y)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [[[1]]]" + Environment.NewLine + + "Actual: [[[1]]]", + ex.Message + ); + } + + [Fact] + public void CollectionDepth_NotEqual() + { + var x = new List { new List { new List { 1 } } }; + var y = new List { new List { new List { 2 } } }; + + Assert.NotEqual(x, y); + } + + [Fact] + public void StringArray_ObjectArray_Equal() + { + var expected = new string[] { "foo", "bar" }; + var actual = new object[] { "foo", "bar" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not string[] [\"foo\", \"bar\"]" + Environment.NewLine + + "Actual: object[] [\"foo\", \"bar\"]", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void StringArray_ObjectArray_NotEqual() + { + var expected = new string[] { "foo", "bar" }; + var actual = new object[] { "foo", "baz" }; + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (object)actual); + } + + [Fact] + public void MultidimensionalArrays_Equal() + { + var expected = new int[,] { { 1 }, { 2 } }; + var actual = new int[,] { { 1 }, { 2 } }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + // TODO: Would be better to have formatting that preserves the ranks instead of + // flattening, which happens because multi-dimensional arrays enumerate flatly + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [1, 2]" + Environment.NewLine + + "Actual: [1, 2]", + ex.Message + ); + } + + [Fact] + public void MultidimensionalArrays_NotEqual() + { + var expected = new int[,] { { 1, 2 } }; + var actual = new int[,] { { 1 }, { 2 } }; + + Assert.NotEqual(expected, actual); + } + +#if !XUNIT_AOT // Array.CreateInstance is not available in Native AOT + + [Fact] + public void NonZeroBoundedArrays_Equal() + { + var expected = Array.CreateInstance(typeof(int), [1], [1]); + expected.SetValue(42, 1); + var actual = Array.CreateInstance(typeof(int), [1], [1]); + actual.SetValue(42, 1); + + var ex = Record.Exception(() => Assert.NotEqual(expected, (object)actual)); + + Assert.IsType(ex); + // TODO: Would be better to have formatting that shows the non-zero bounds + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [42]" + Environment.NewLine + + "Actual: [42]", + ex.Message + ); + } + + [Fact] + public void NonZeroBoundedArrays_NotEqual() + { + var expected = Array.CreateInstance(typeof(int), [1], [1]); + expected.SetValue(42, 1); + var actual = Array.CreateInstance(typeof(int), [1], [0]); + actual.SetValue(42, 0); + + Assert.NotEqual(expected, actual); + } + +#endif // !XUNIT_AOT + + [Fact] + public void CollectionWithIEquatable_Equal() + { + var expected = new EnumerableEquatable { 42, 2112 }; + var actual = new EnumerableEquatable { 2112, 42 }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [42, 2112]" + Environment.NewLine + + "Actual: [2112, 42]", + ex.Message + ); + } + + [Fact] + public void CollectionWithIEquatable_NotEqual() + { + var expected = new EnumerableEquatable { 42, 2112 }; + var actual = new EnumerableEquatable { 2112, 2600 }; + + Assert.NotEqual(expected, actual); + } + + public sealed class EnumerableEquatable : + IEnumerable, IEquatable> + { + readonly List values = []; + + public void Add(T value) => values.Add(value); + + public bool Equals(EnumerableEquatable? other) + { + if (other == null) + return false; + + return !values.Except(other.values).Any() && !other.values.Except(values).Any(); + } + + public override bool Equals(object? obj) => + Equals(obj as EnumerableEquatable); + + public IEnumerator GetEnumerator() => + values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + public override int GetHashCode() => + values.GetHashCode(); + } + } + + public class Dictionaries + { + [Fact] + public void SameTypes_Equal() + { + var expected = new Dictionary { ["foo"] = "bar" }; + var actual = new Dictionary { ["foo"] = "bar" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Dictionaries are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [[foo, bar]]" + Environment.NewLine + + "Actual: [[foo, bar]]", +#else + "Expected: Not [[\"foo\"] = \"bar\"]" + Environment.NewLine + + "Actual: [[\"foo\"] = \"bar\"]", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (IDictionary)actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void SameTypes_NotEqual() + { + var expected = new Dictionary { ["foo"] = "bar" }; + var actual = new Dictionary { ["foo"] = "baz" }; + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (IDictionary)actual); + Assert.NotEqual(expected, (object)actual); + } + + [Fact] + public void DifferentTypes_Equal() + { + var expected = new Dictionary { ["foo"] = "bar" }; + var actual = new ConcurrentDictionary(expected); + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not Dictionary [[foo, bar]]" + Environment.NewLine + + "Actual: ConcurrentDictionary [[foo, bar]]", +#else + "Expected: Not Dictionary [[\"foo\"] = \"bar\"]" + Environment.NewLine + + "Actual: ConcurrentDictionary [[\"foo\"] = \"bar\"]", +#endif + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, (IDictionary)actual)); + assertFailure(() => Assert.NotEqual(expected, (object)actual)); + } + + [Fact] + public void DifferentTypes_NotEqual() + { + var expected = new Dictionary { ["foo"] = "bar" }; + var actual = new ConcurrentDictionary { ["foo"] = "baz" }; + + Assert.NotEqual(expected, (IDictionary)actual); + Assert.NotEqual(expected, (object)actual); + } + + [Fact] + public void NullValue_Equal() + { + var expected = new Dictionary { { "two", null } }; + var actual = new Dictionary { { "two", null } }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Dictionaries are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [[two, ]]" + Environment.NewLine + + "Actual: [[two, ]]", +#else + "Expected: Not [[\"two\"] = null]" + Environment.NewLine + + "Actual: [[\"two\"] = null]", +#endif + ex.Message + ); + } + + [Fact] + public void NullValue_NotEqual() + { + var expected = new Dictionary { { "two", null } }; + var actual = new Dictionary { { "two", 1 } }; + + Assert.NotEqual(expected, actual); + } + } + + public class HashSets + { + [Fact] + public static void InOrder_Equal() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 3 }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: HashSets are equal" + Environment.NewLine + + "Expected: Not [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3]", + ex.Message + ); + } + + [Fact] + public static void InOrder_NotEqual() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 4 }; + + Assert.NotEqual(expected, actual); + } + + [Fact] + public static void OutOfOrder_Equal() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 2, 3, 1 }; + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: HashSets are equal" + Environment.NewLine + + "Expected: Not [1, 2, 3]" + Environment.NewLine + + "Actual: [2, 3, 1]", + ex.Message + ); + } + + [Fact] + public static void OutOfOrder_NotEqual() + { + var expected = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 2, 4, 1 }; + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void DifferentTypes_Equal() + { + var expected = new HashSet { "bar", "foo" }; + var actual = new SortedSet { "foo", "bar" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Sets are equal" + Environment.NewLine + + "Expected: Not HashSet [\"bar\", \"foo\"]" + Environment.NewLine + + "Actual: SortedSet [\"bar\", \"foo\"]", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.NotEqual(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void DifferentTypes_NotEqual() + { + object expected = new HashSet { 42 }; + object actual = new HashSet { 42L }; + + Assert.NotEqual(expected, actual); + } + +#if !XUNIT_AOT // Comparer func overload is disabled in AOT via compiler + + [Fact] + public void ComparerFunc_Throws() + { + var expected = new HashSet { "bar" }; + var actual = new HashSet { "baz" }; + +#pragma warning disable xUnit2026 // Comparison of sets must be done with IEqualityComparer + var ex = Record.Exception(() => Assert.NotEqual(expected, actual, (string l, string r) => false)); +#pragma warning restore xUnit2026 // Comparison of sets must be done with IEqualityComparer + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: During comparison of two collections, GetHashCode was called, but only a comparison function was provided. This typically indicates trying to compare two sets with an item comparison function, which is not supported. For more information, see https://xunit.net/docs/hash-sets-vs-linear-containers", + ex.Message + ); + } + +#endif // !XUNIT_AOT + } + + public class Sets + { + [Fact] + public void InOrder_Equal() + { + var expected = new NonGenericSet { "bar", "foo" }; + var actual = new NonGenericSet { "bar", "foo" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Sets are equal" + Environment.NewLine + + "Expected: Not [\"bar\", \"foo\"]" + Environment.NewLine + + "Actual: [\"bar\", \"foo\"]", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.NotEqual(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void InOrder_NotEqual() + { + var expected = new NonGenericSet { "bar", "foo" }; + var actual = new NonGenericSet { "bar", "baz" }; + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.NotEqual(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void OutOfOrder_Equal() + { + var expected = new NonGenericSet { "bar", "foo" }; + var actual = new NonGenericSet { "foo", "bar" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Sets are equal" + Environment.NewLine + + "Expected: Not [\"bar\", \"foo\"]" + Environment.NewLine + + "Actual: [\"foo\", \"bar\"]", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.NotEqual(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void OutOfOrder_NotEqual() + { + var expected = new NonGenericSet { "bar", "foo" }; + var actual = new NonGenericSet { "foo", "baz" }; + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.NotEqual(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void DifferentContents() + { + var expected = new NonGenericSet { "bar" }; + var actual = new NonGenericSet { "bar", "foo" }; + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.NotEqual(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void DifferentTypes_Equal() + { + var expected = new NonGenericSet { "bar" }; + var actual = new HashSet { "bar" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Sets are equal" + Environment.NewLine + + "Expected: Not NonGenericSet [\"bar\"]" + Environment.NewLine + + "Actual: HashSet [\"bar\"]", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.NotEqual(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void DifferentTypes_NotEqual() + { + var expected = new NonGenericSet { "bar" }; + var actual = new HashSet { "baz" }; + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.NotEqual(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void TwoGenericSubClass_Equal() + { + var expected = new TwoGenericSet { "foo", "bar" }; + var actual = new TwoGenericSet { "foo", "bar" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Sets are equal" + Environment.NewLine + + "Expected: Not [\"foo\", \"bar\"]" + Environment.NewLine + + "Actual: [\"foo\", \"bar\"]", + ex.Message + ); + } + + assertFailure(() => Assert.NotEqual(expected, actual)); + assertFailure(() => Assert.NotEqual(expected, (ISet)actual)); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + assertFailure(() => Assert.NotEqual(expected, (object)actual)); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + + [Fact] + public void TwoGenericSubClass_NotEqual() + { + var expected = new TwoGenericSet { "foo", "bar" }; + var actual = new TwoGenericSet { "foo", "baz" }; + + Assert.NotEqual(expected, actual); + Assert.NotEqual(expected, (ISet)actual); +#if !XUNIT_AOT // AOT can't detect sets dynamically so it just treats it like a linear collection +#pragma warning disable xUnit2027 // Comparison of sets to linear containers have undefined results + Assert.NotEqual(expected, (object)actual); +#pragma warning restore xUnit2027 // Comparison of sets to linear containers have undefined results +#endif + } + +#if !XUNIT_AOT // Comparer func overload is disabled in AOT via compiler + + [Fact] + public void ComparerFunc_Throws() + { + var expected = new NonGenericSet { "bar" }; + var actual = new HashSet { "baz" }; + +#pragma warning disable xUnit2026 // Comparison of sets must be done with IEqualityComparer + var ex = Record.Exception(() => Assert.NotEqual(expected, actual, (string l, string r) => false)); +#pragma warning restore xUnit2026 // Comparison of sets must be done with IEqualityComparer + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: During comparison of two collections, GetHashCode was called, but only a comparison function was provided. This typically indicates trying to compare two sets with an item comparison function, which is not supported. For more information, see https://xunit.net/docs/hash-sets-vs-linear-containers", + ex.Message + ); + } + +#endif // !XUNIT_AOT + } + + // https://github.com/xunit/xunit/issues/3137 + public class ImmutableArrays + { + [Fact] + public void Equal() + { + var expected = new[] { 1, 2, 3 }.ToImmutableArray(); + var actual = new[] { 1, 2, 3 }.ToImmutableArray(); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3]", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + var expected = new[] { 1, 2, 3 }.ToImmutableArray(); + var actual = new[] { 1, 2, 4 }.ToImmutableArray(); + + Assert.NotEqual(expected, actual); + } + } + + public class Strings + { + [Fact] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual("actual", "actual")); + + Assert.IsType(ex); + Assert.Equal( + @"Assert.NotEqual() Failure: Strings are equal" + Environment.NewLine + + @"Expected: Not ""actual""" + Environment.NewLine + + @"Actual: ""actual""", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual("foo", "bar"); + } + + [Fact] + public void Truncation() + { + var ex = Record.Exception( + () => Assert.NotEqual( + "This is a long string so that we can test truncation behavior", + "This is a long string so that we can test truncation behavior" + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Strings are equal" + Environment.NewLine + + @"Expected: Not ""This is a long string so that we can test truncati""···" + Environment.NewLine + + @"Actual: ""This is a long string so that we can test truncati""···", + ex.Message + ); + } + } + + public class KeyValuePair + { +#if !XUNIT_AOT // AOT can't deep dive into collections in the same was a non-AOT + + [Fact] + public void CollectionKeys_Equal() + { + // Different concrete collection types in the key slot, per https://github.com/xunit/xunit/issues/2850 +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + var expected = new KeyValuePair, int>(new List { "Key1", "Key2" }, 42); + var actual = new KeyValuePair, int>(new string[] { "Key1", "Key2" }, 42); +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [System.Collections.Generic.List`1[System.String], 42]" + Environment.NewLine + + "Actual: [System.String[], 42]", +#else + "Expected: Not [[\"Key1\", \"Key2\"]] = 42" + Environment.NewLine + + "Actual: [[\"Key1\", \"Key2\"]] = 42", +#endif + ex.Message + ); + } + + [Fact] + public void CollectionKeys_NotEqual() + { + // Different concrete collection types in the key slot, per https://github.com/xunit/xunit/issues/2850 +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + var expected = new KeyValuePair, int>(new List { "Key1", "Key2" }, 42); + var actual = new KeyValuePair, int>(new string[] { "Key1", "Key3" }, 42); +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void CollectionValues_Equal() + { + // Different concrete collection types in the key slot, per https://github.com/xunit/xunit/issues/2850 +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + var expected = new KeyValuePair>("Key1", new List { "Value1a", "Value1b" }); + var actual = new KeyValuePair>("Key1", new string[] { "Value1a", "Value1b" }); +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [Key1, System.Collections.Generic.List`1[System.String]]" + Environment.NewLine + + "Actual: [Key1, System.String[]]", +#else + "Expected: Not [\"Key1\"] = [\"Value1a\", \"Value1b\"]" + Environment.NewLine + + "Actual: [\"Key1\"] = [\"Value1a\", \"Value1b\"]", +#endif + ex.Message + ); + } + + [Fact] + public void CollectionValues_NotEqual() + { + // Different concrete collection types in the key slot, per https://github.com/xunit/xunit/issues/2850 +#pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable IDE0300 // Simplify collection initialization + var expected = new KeyValuePair>("Key1", new List { "Value1a", "Value1b" }); + var actual = new KeyValuePair>("Key1", new string[] { "Value1a", "Value2a" }); +#pragma warning restore IDE0300 // Simplify collection initialization +#pragma warning restore IDE0028 // Simplify collection initialization + + Assert.NotEqual(expected, actual); + } + +#endif // !XUNIT_AOT + + [Fact] + public void EquatableKeys_Equal() + { + var expected = new KeyValuePair(new() { Char = 'a' }, 42); + var actual = new KeyValuePair(new() { Char = 'a' }, 42); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [EqualityAssertsTests+NotEqual+KeyValuePair+EquatableObject, 42]" + Environment.NewLine + + "Actual: [EqualityAssertsTests+NotEqual+KeyValuePair+EquatableObject, 42]", +#else + "Expected: Not [EquatableObject { Char = 'a' }] = 42" + Environment.NewLine + + "Actual: [EquatableObject { Char = 'a' }] = 42", +#endif + ex.Message + ); + } + + [Fact] + public void EquatableKeys_NotEqual() + { + var expected = new KeyValuePair(new() { Char = 'a' }, 42); + var actual = new KeyValuePair(new() { Char = 'b' }, 42); + + Assert.NotEqual(expected, actual); + } + + [Fact] + public void EquatableValues_Equal() + { + var expected = new KeyValuePair("Key1", new() { Char = 'a' }); + var actual = new KeyValuePair("Key1", new() { Char = 'a' }); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + "Expected: Not [Key1, EqualityAssertsTests+NotEqual+KeyValuePair+EquatableObject]" + Environment.NewLine + + "Actual: [Key1, EqualityAssertsTests+NotEqual+KeyValuePair+EquatableObject]", +#else + "Expected: Not [\"Key1\"] = EquatableObject { Char = 'a' }" + Environment.NewLine + + "Actual: [\"Key1\"] = EquatableObject { Char = 'a' }", +#endif + ex.Message + ); + } + + [Fact] + public void EquatableValues_NotEqual() + { + var expected = new KeyValuePair("Key1", new() { Char = 'a' }); + var actual = new KeyValuePair("Key1", new() { Char = 'b' }); + + Assert.NotEqual(expected, actual); + } + + public class EquatableObject : IEquatable + { + public char Char { get; set; } + + public bool Equals(EquatableObject? other) => + other != null && other.Char == Char; + + public override bool Equals(object? obj) => + Equals(obj as EquatableObject); + + public override int GetHashCode() => + Char.GetHashCode(); + } + } + + public class DoubleEnumerationPrevention + { + [Fact] + public static void EnumeratesOnlyOnce_Equal() + { + var expected = new RunOnceEnumerable([1, 2, 3, 4, 5]); + var actual = new RunOnceEnumerable([1, 2, 3, 4, 5]); + + var ex = Record.Exception(() => Assert.NotEqual(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine + + "Expected: Not [1, 2, 3, 4, 5]" + Environment.NewLine + + "Actual: [1, 2, 3, 4, 5]", + ex.Message + ); + } + + [Fact] + public static void EnumeratesOnlyOnce_NotEqual() + { + var expected = new RunOnceEnumerable([1, 2, 3, 4, 5]); + var actual = new RunOnceEnumerable([1, 2, 3, 4, 5, 6]); + + Assert.NotEqual(expected, actual); + } + } + } + + public class NotEqual_Decimal + { + [CulturedFactDefault] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(0.11111M, 0.11444M, 2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are equal" + Environment.NewLine + + $"Expected: Not {0.11M} (rounded from {0.11111})" + Environment.NewLine + + $"Actual: {0.11M} (rounded from {0.11444})", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(0.11111M, 0.11444M, 3); + } + } + + public class NotEqual_Double + { + public class WithPrecision + { + [CulturedFactDefault] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(0.11111, 0.11444, 2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are within 2 decimal places" + Environment.NewLine + + $"Expected: Not {0.11:G17} (rounded from {0.11111:G17})" + Environment.NewLine + + $"Actual: {0.11:G17} (rounded from {0.11444:G17})", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(0.11111, 0.11444, 3); + } + } + + public class WithMidPointRounding + { + [CulturedFactDefault] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(10.565, 10.566, 2, MidpointRounding.AwayFromZero)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotEqual() Failure: Values are within 2 decimal places" + Environment.NewLine + + $"Expected: Not {10.57:G17} (rounded from {10.565:G17})" + Environment.NewLine + + $"Actual: {10.57:G17} (rounded from {10.566:G17})", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(0.11113, 0.11115, 4, MidpointRounding.ToEven); + } + } + + public class WithTolerance + { + [Fact] + public void GuardClause() + { + var ex = Record.Exception(() => Assert.NotEqual(0.0, 1.0, double.NegativeInfinity)); + + var argEx = Assert.IsType(ex); + Assert.StartsWith("Tolerance must be greater than or equal to zero", ex.Message); + Assert.Equal("tolerance", argEx.ParamName); + } + + [CulturedFactDefault] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(10.566, 10.565, 0.01)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotEqual() Failure: Values are within tolerance {0.01:G17}" + Environment.NewLine + + $"Expected: Not {10.566:G17}" + Environment.NewLine + + $"Actual: {10.565:G17}", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(0.11113, 0.11115, 0.00001); + } + + [CulturedFactDefault] + public void NaN_Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(double.NaN, double.NaN, 1000.0)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotEqual() Failure: Values are within tolerance {1000.0:G17}" + Environment.NewLine + + $"Expected: Not {double.NaN}" + Environment.NewLine + + $"Actual: {double.NaN}", + ex.Message + ); + } + + [Fact] + public void NaN_NotEqual() + { + Assert.NotEqual(20210102.2208, double.NaN, 20000000.0); + } + + [CulturedFactDefault] + public void InfiniteTolerance_Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(double.MinValue, double.MaxValue, double.PositiveInfinity)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotEqual() Failure: Values are within tolerance {double.PositiveInfinity}" + Environment.NewLine + + $"Expected: Not {double.MinValue:G17}" + Environment.NewLine + + $"Actual: {double.MaxValue:G17}", + ex.Message + ); + } + + [Fact] + public void PositiveInfinity_NotEqual() + { + Assert.NotEqual(double.PositiveInfinity, 77.7, 1.0); + } + + [Fact] + public void NegativeInfinity_NotEqual() + { + Assert.NotEqual(0.0, double.NegativeInfinity, 1.0); + } + } + } + + public class NotEqual_Float + { + public class WithPrecision + { + [CulturedFactDefault] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(0.11111f, 0.11444f, 2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are within 2 decimal places" + Environment.NewLine + + $"Expected: Not {0.11:G9} (rounded from {0.11111f:G9})" + Environment.NewLine + + $"Actual: {0.11:G9} (rounded from {0.11444f:G9})", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(0.11111f, 0.11444f, 3); + } + } + + public class WithMidPointRounding + { + [CulturedFactDefault] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(10.5655f, 10.5666f, 2, MidpointRounding.AwayFromZero)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotEqual() Failure: Values are within 2 decimal places" + Environment.NewLine + + $"Expected: Not {10.57:G9} (rounded from {10.5655f:G9})" + Environment.NewLine + + $"Actual: {10.57:G9} (rounded from {10.5666f:G9})", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(0.111133f, 0.111155f, 4, MidpointRounding.ToEven); + } + } + + public class WithTolerance + { + [Fact] + public void GuardClause() + { + var ex = Record.Exception(() => Assert.NotEqual(0.0f, 1.0f, float.NegativeInfinity)); + + var argEx = Assert.IsType(ex); + Assert.StartsWith("Tolerance must be greater than or equal to zero", ex.Message); + Assert.Equal("tolerance", argEx.ParamName); + } + + [CulturedFactDefault] + public void Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(10.569f, 10.562f, 0.01f)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotEqual() Failure: Values are within tolerance {0.01f:G9}" + Environment.NewLine + + $"Expected: Not {10.569f:G9}" + Environment.NewLine + + $"Actual: {10.562f:G9}", + ex.Message + ); + } + + [Fact] + public void NotEqual() + { + Assert.NotEqual(0.11113f, 0.11115f, 0.00001f); + } + + [CulturedFactDefault] + public void NaN_Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(float.NaN, float.NaN, 1000.0f)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotEqual() Failure: Values are within tolerance {1000.0f:G9}" + Environment.NewLine + + "Expected: Not NaN" + Environment.NewLine + + "Actual: NaN", + ex.Message + ); + } + + [Fact] + public void NaN_NotEqual() + { + Assert.NotEqual(20210102.2208f, float.NaN, 20000000.0f); + } + + [CulturedFactDefault] + public void InfiniteTolerance_Equal() + { + var ex = Record.Exception(() => Assert.NotEqual(float.MinValue, float.MaxValue, float.PositiveInfinity)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotEqual() Failure: Values are within tolerance {float.PositiveInfinity}" + Environment.NewLine + + $"Expected: Not {float.MinValue:G9}" + Environment.NewLine + + $"Actual: {float.MaxValue:G9}", + ex.Message + ); + } + + [Fact] + public void PositiveInfinity_NotEqual() + { + Assert.NotEqual(float.PositiveInfinity, 77.7f, 1.0f); + } + + [Fact] + public void NegativeInfinity_NotEqual() + { + Assert.NotEqual(0.0f, float.NegativeInfinity, 1.0f); + } + } + } + + public class NotStrictEqual + { + [Fact] + public static void Equal() + { + var ex = Record.Exception(() => Assert.NotStrictEqual("actual", "actual")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotStrictEqual() Failure: Values are equal" + Environment.NewLine + + @"Expected: Not ""actual""" + Environment.NewLine + + @"Actual: ""actual""", + ex.Message + ); + } + + [Fact] + public static void NotEqual_Strings() + { + Assert.NotStrictEqual("bob", "jim"); + } + + [Fact] + public static void NotEqual_Classes() + { + Assert.NotStrictEqual(new EnumerableClass("ploeh"), new EnumerableClass("fnaah")); + } + + [Fact] + public static void DifferentTypes_Equal() + { + var ex = Record.Exception(() => Assert.NotStrictEqual(new DerivedClass(), new BaseClass())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotStrictEqual() Failure: Values are equal" + Environment.NewLine + +#if XUNIT_AOT + $"Expected: Not DerivedClass {{ {ArgumentFormatter.Ellipsis} }}" + Environment.NewLine + + $"Actual: BaseClass {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Expected: Not DerivedClass { }" + Environment.NewLine + + "Actual: BaseClass { }", +#endif + ex.Message + ); + } + } + + public class StrictEqual + { + [Fact] + public static void Equal() + { +#pragma warning disable xUnit2006 // Do not use invalid string equality check + Assert.StrictEqual("actual", "actual"); +#pragma warning restore xUnit2006 // Do not use invalid string equality check + } + + [Fact] + public static void NotEqual_Strings() + { +#pragma warning disable xUnit2006 // Do not use invalid string equality check + var ex = Record.Exception(() => Assert.StrictEqual("bob", "jim")); +#pragma warning restore xUnit2006 // Do not use invalid string equality check + + Assert.IsType(ex); + Assert.Equal( + "Assert.StrictEqual() Failure: Values differ" + Environment.NewLine + + @"Expected: ""bob""" + Environment.NewLine + + @"Actual: ""jim""", + ex.Message + ); + } + + [Fact] + public static void NotEqual_Classes() + { +#pragma warning disable xUnit2006 // TODO: Fix this analyzer in the face of non-generic StrictEqual + var ex = Record.Exception(() => Assert.StrictEqual(new EnumerableClass("ploeh"), new EnumerableClass("fnaah"))); +#pragma warning restore xUnit2006 + + Assert.IsType(ex); + Assert.Equal( + "Assert.StrictEqual() Failure: Values differ" + Environment.NewLine + + $"Expected: [{ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + $"Actual: [{ArgumentFormatter.Ellipsis}]", + ex.Message + ); + } + + [Fact] + public static void DifferentTypes_Equal() + { +#pragma warning disable xUnit2006 // TODO: Fix this analyzer in the face of non-generic StrictEqual + Assert.StrictEqual(new DerivedClass(), new BaseClass()); +#pragma warning restore xUnit2006 + } + } + + class BaseClass { } + + class DerivedClass : BaseClass + { + public override bool Equals(object? obj) => + obj is BaseClass || base.Equals(obj); + + public override int GetHashCode() => 0; + } + + class EnumerableClass(string _, params BaseClass[] bars) : + IEnumerable + { + readonly string _ = _; + readonly IEnumerable bars = bars; + + public IEnumerator GetEnumerator() => bars.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + class MultiComparable(int value) : + IComparable + { + public int Value { get; } = value; + + public int CompareTo(object? obj) + { + if (obj is int intObj) + return Value.CompareTo(intObj); + else if (obj is MultiComparable multiObj) + return Value.CompareTo(multiObj.Value); + + throw new InvalidOperationException(); + } + } + + class ComparableBaseClass(int value) : + IComparable + { + public int Value { get; } = value; + + public int CompareTo(ComparableBaseClass? other) => Value.CompareTo(other!.Value); + } + + class ComparableSubClassA(int value) : + ComparableBaseClass(value) + { } + + class ComparableSubClassB(int value) : + ComparableBaseClass(value) + { } + + class ComparableThrower(int value) : + IComparable + { + public int Value { get; } = value; + + public int CompareTo(ComparableThrower? other) => + throw new InvalidOperationException(); + + public override bool Equals(object? obj) => + Value == ((ComparableThrower?)obj)!.Value; + + public override int GetHashCode() => + Value; + } + + class EquatableBaseClass(int value) : + IEquatable + { + public int Value { get; } = value; + + public bool Equals(EquatableBaseClass? other) => + Value == other!.Value; + + public override bool Equals(object? obj) => + Equals(obj as EquatableBaseClass); + + public override int GetHashCode() => + Value.GetHashCode(); + } + + class EquatableSubClassA(int value) : + EquatableBaseClass(value) + { } + + class EquatableSubClassB(int value) : + EquatableBaseClass(value) + { } + +#pragma warning disable CA1067 // Override Object.Equals(object) when implementing IEquatable + + class StringWrapper(string value) : + IEquatable + { + public string Value { get; } = value; + + bool IEquatable.Equals(StringWrapper? other) => + Value == other!.Value; + } + +#pragma warning restore CA1067 // Override Object.Equals(object) when implementing IEquatable + + class StructuralStringWrapper(string value) : + IStructuralEquatable + { + public string Value { get; } = value; + + public bool Equals( + object? other, + IEqualityComparer comparer) + { + if (other is not StructuralStringWrapper otherWrapper) + return false; + + return comparer.Equals(Value, otherWrapper.Value); + } + + public int GetHashCode(IEqualityComparer comparer) => + Value.GetHashCode(); + } + + class NonGenericSet : HashSet { } + + class TwoGenericSet : HashSet { } + + class ImplicitIEquatableExpected(int value) : + IEquatable + { + public int Value { get; } = value; + + public bool Equals(IntWrapper? other) => Value == other!.Value; + } + + class ExplicitIEquatableExpected(int value) : + IEquatable + { + public int Value { get; } = value; + + bool IEquatable.Equals(IntWrapper? other) => Value == other!.Value; + } + + class ImplicitIComparableExpected(int value) : + IComparable + { + public int Value { get; } = value; + + public int CompareTo(IntWrapper? other) => Value.CompareTo(other!.Value); + } + + class ExplicitIComparableActual(int value) : + IComparable + { + public int Value { get; } = value; + + int IComparable.CompareTo(IntWrapper? other) => Value.CompareTo(other!.Value); + } + + class IComparableActualThrower(int value) : + IComparable + { + public int Value { get; } = value; + + public int CompareTo(IntWrapper? other) => + throw new NotSupportedException(); + + public override bool Equals(object? obj) => Value == ((IntWrapper?)obj)!.Value; + + public override int GetHashCode() => Value; + } + + class IntWrapper(int value) + { + public int Value { get; } = value; + } + + class SpyComparable(int result) : + IComparable + { + public bool CompareCalled; + + public int CompareTo(object? obj) + { + CompareCalled = true; + return result; + } + } + + class SpyComparable_Generic(int result = 0) : + IComparable + { + public bool CompareCalled; + + public int CompareTo(SpyComparable_Generic? other) + { + CompareCalled = true; + return result; + } + } + + class SpyEquatable(bool result = true) : + IEquatable + { + public bool Equals__Called; + public SpyEquatable? Equals_Other; + + public bool Equals(SpyEquatable? other) + { + Equals__Called = true; + Equals_Other = other; + + return result; + } + + public override bool Equals(object? obj) => + Equals(obj as SpyEquatable); + + public override int GetHashCode() => + 42; + } + + class NonComparableObject(bool result = true) + { + public override bool Equals(object? obj) => + result; + + public override int GetHashCode() => + 42; + } + + sealed class RunOnceEnumerable(IEnumerable source) : + IEnumerable + { + private bool _called; + + public IEnumerable Source { get; } = source; + + public IEnumerator GetEnumerator() + { + Assert.False(_called, "GetEnumerator() was called more than once"); + _called = true; + return Source.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EquivalenceAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EquivalenceAssertsTests.cs new file mode 100644 index 00000000000..7ae448ab15c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EquivalenceAssertsTests.cs @@ -0,0 +1,2284 @@ +#if !XUNIT_AOT // Assert.Equivalent is not available in Native AOT + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using Xunit; +using Xunit.Internal; +using Xunit.Sdk; + +public class EquivalenceAssertsTests +{ + public class NullValues + { + [Fact] + public void TwoNullsAreEquivalent() + { + Assert.Equivalent(null, null); + } + + [Theory] + [InlineData(null, 42)] + [InlineData(42, null)] + public void NullIsNotEquivalentToNonNull( + object? expected, + object? actual) + { + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + $"Expected: {expected ?? "null"}" + Environment.NewLine + + $"Actual: {actual ?? "null"}", + ex.Message + ); + } + } + + public class ValueTypes + { + [Theory] + [InlineData(42, 42)] + [InlineData(2112L, 2112L)] + [InlineData('a', 'a')] + [InlineData(1.1f, 1.1f)] + [InlineData(1.1, 1.1)] + [InlineData(true, true)] + [InlineData(ConsoleKey.A, ConsoleKey.A)] + public void SameType_Success( + object? expected, + object? actual) + { + Assert.Equivalent(expected, actual); + } + + [Fact] + public void SameType_Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(12, 13)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: 12" + Environment.NewLine + + "Actual: 13", + ex.Message + ); + } + + [Fact] + public void SameValueFromDifferentIntrinsicTypes_Success() + { + Assert.Equivalent(12, 12L); + } + + // https://github.com/xunit/xunit/issues/2913 + [Fact] + public void Decimals_Success() + { + Assert.Equivalent(1m, 1m); + } + + // https://github.com/xunit/xunit/issues/2913 + [Fact] + public void Decimals_Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(1m, 2m)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: 1" + Environment.NewLine + + "Actual: 2", + ex.Message + ); + } + + // https://github.com/xunit/xunit/issues/2913 + [Fact] + public void IntrinsicPlusNonIntrinsic_Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(1m, new object())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: 1" + Environment.NewLine + + "Actual: Object { }", + ex.Message + ); + } + + public class Guids + { + // https://github.com/xunit/xunit/issues/2974 + [Fact] + public void SameType_Success() + { + Assert.Equivalent(new Guid("b727762b-a1c1-49a0-b045-59ba97b17b61"), new Guid("b727762b-a1c1-49a0-b045-59ba97b17b61")); + } + + // https://github.com/xunit/xunit/issues/2974 + [Fact] + public void SameType_Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(new Guid("b727762b-a1c1-49a0-b045-59ba97b17b61"), new Guid("963ff9f5-cb83-480e-85ea-7e8950a01f00"))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: b727762b-a1c1-49a0-b045-59ba97b17b61" + Environment.NewLine + + "Actual: 963ff9f5-cb83-480e-85ea-7e8950a01f00", + ex.Message + ); + } + + // https://github.com/xunit/xunit/issues/2974 + [Fact] + public void IntrinsicPlusNonIntrinsic_Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(new Guid("b727762b-a1c1-49a0-b045-59ba97b17b61"), new object())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: b727762b-a1c1-49a0-b045-59ba97b17b61" + Environment.NewLine + + "Actual: Object { }", + ex.Message + ); + } + } + } + + public class NullableValueTypes + { + [Fact] + public void Success() + { + int? expected = 42; + int? actual = 42; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void Failure() + { + int? expected = 42; + int? actual = 2112; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + } + + public class ValueTypes_Identical_Deep + { + [Fact] + public void Success() + { + var expected = new DeepStruct(new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }); + var actual = new DeepStruct(new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void Failure() + { + var expected = new DeepStruct(new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }); + var actual = new DeepStruct(new ShallowClass { Value1 = 13, Value2 = "Hello, world!" }); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Shallow.Value1'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 13", + ex.Message + ); + } + } + + public class Strings + { + [Fact] + public void Success() + { + Assert.Equivalent("Hello", "Hello"); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Equivalent("Hello, world", "Hello, world!")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: \"Hello, world\"" + Environment.NewLine + + "Actual: \"Hello, world!\"", + ex.Message + ); + } + + [Fact] + public void NullIsNotEquivalentToEmptyString() + { + var ex = Record.Exception(() => Assert.Equivalent(null, string.Empty)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: null" + Environment.NewLine + + "Actual: \"\"", + ex.Message + ); + } + } + + public class AnonymousTypes_Identical_Shallow + { + [Fact] + public void Success() + { + Assert.Equivalent(new { x = 42 }, new { x = 42 }); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(new { x = 42 }, new { x = 2112 })); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'x'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + } + + public class AnonymousTypes_Identical_Deep + { + [Fact] + public void Success() + { + Assert.Equivalent(new { x = new { y = 42 } }, new { x = new { y = 42 } }); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(new { x = new { y = 42 } }, new { x = new { y = 2112 } })); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'x.y'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + } + + public class AnonymousTypes_Compatible_Shallow + { + [Fact] + public void Success() + { + Assert.Equivalent(new { x = 42, y = 2112 }, new { y = 2112, x = 42 }); + } + + [Fact] + public void Success_IgnorePrivateValue() + { + var expected = new PrivateMembersClass(1, "help"); + var actual = new PrivateMembersClass(2, "me"); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(new { x = 42, y = 2600 }, new { y = 2600, x = 2112 })); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'x'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + } + + public class AnonymousTypes_Compatible_Deep + { + [Fact] + public void Success() + { + Assert.Equivalent(new { x = new { y = 2112 }, z = 42 }, new { z = 42, x = new { y = 2112 } }); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(new { x = new { y = 2600 }, z = 42 }, new { z = 42, x = new { y = 2112 } })); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'x.y'" + Environment.NewLine + + "Expected: 2600" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + } + + public class ComplexTypes_Identical_Shallow_NotStructuralEquatable + { + [Fact] + public void Success() + { + var expected = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }; + var actual = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void Success_IgnoreStaticValue() + { + try + { + ShallowClass.StaticValue = 1; + ShallowClass2.StaticValue = 2; + + var expected = new ShallowClass(); + var actual = new ShallowClass2(); + + Assert.Equivalent(expected, actual); + } + finally + { + ShallowClass.StaticValue = default; + ShallowClass2.StaticValue = default; + } + } + + [Fact] + public void Failure() + { + var expected = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }; + var actual = new ShallowClass { Value1 = 2112, Value2 = "Hello, world!" }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Value1'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + } + + public class ComplexTypes_Identical_Deep_NotStructuralEquatable + { + [Fact] + public void Success() + { + var expected = new DeepClass { Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }, Value3 = 21.12m }; + var actual = new DeepClass { Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }, Value3 = 21.12m }; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void Failure() + { + var expected = new DeepClass { Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }, Value3 = 21.12m }; + var actual = new DeepClass { Shallow = new ShallowClass { Value1 = 2600, Value2 = "Hello, world!" }, Value3 = 21.12m }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Shallow.Value1'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2600", + ex.Message + ); + } + } + + public class ComplexTypes_Compatible_Shallow_NotStructuralEquatable + { + [Fact] + public void Success() + { + var expected = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }; + var actual = new ShallowClass2 { Value1 = 42, Value2 = "Hello, world!" }; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void Failure() + { + var expected = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }; + var actual = new ShallowClass2 { Value1 = 2112, Value2 = "Hello, world!" }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Value1'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + } + + public class ComplexTypes_Compatible_Deep_NotStructuralEquatable + { + [Fact] + public void Success() + { + var expected = new DeepClass { Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }, Value3 = 21.12m }; + var actual = new DeepClass2 { Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }, Value3 = 21.12m }; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void Failure() + { + var expected = new DeepClass { Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" }, Value3 = 21.12m }; + var actual = new DeepClass2 { Shallow = new ShallowClass { Value1 = 2600, Value2 = "Hello, world!" }, Value3 = 21.12m }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Shallow.Value1'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2600", + ex.Message + ); + } + } + + public class MixedComplexAndAnonymousTypes + { + [Fact] + public void Success() + { + var expected = new { Shallow = new { Value1 = 42, Value2 = "Hello, world!" }, Value3 = 21.12m }; + var actual = new DeepClass { Value3 = 21.12m, Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" } }; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void Failure() + { + var expected = new { Shallow = new { Value1 = 42, Value2 = "Hello, world" }, Value3 = 21.12m }; + var actual = new DeepClass { Value3 = 21.12m, Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello, world!" } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Shallow.Value2'" + Environment.NewLine + + "Expected: \"Hello, world\"" + Environment.NewLine + + "Actual: \"Hello, world!\"", + ex.Message + ); + } + } + + public class MismatchedMembers_NotStrict + { + [Fact] + public void Shallow_Success() + { + // Expected can be subset of Actual when strict is false + Assert.Equivalent( + new { x = 42 }, + new { x = 42, y = 2112 }, + strict: false + ); + } + + [Fact] + public void Deep_Success() + { + // Expected can be subset of Actual when strict is false + Assert.Equivalent( + new { w = 42, x = new { y = 2112 } }, + new { w = 42, x = new { y = 2112, z = 2600 } }, + strict: false + ); + } + + [Fact] + public void Shallow_Failure() + { + // Expected can never be superset of Actual + var ex = Record.Exception( + () => Assert.Equivalent( + new { x = 42, y = 2112 }, + new { x = 42, z = 2112 }, + strict: false + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched member list" + Environment.NewLine + + "Expected: [\"x\", \"y\"]" + Environment.NewLine + + "Actual: [\"x\", \"z\"]", + ex.Message + ); + } + + [Fact] + public void Deep_Failure() + { + // Expected can never be superset of Actual + var ex = Record.Exception( + () => Assert.Equivalent( + new { w = 42, x = new { y = 2112 } }, + new { w = 42, x = new { z = 2112 } }, + strict: false + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched member list" + Environment.NewLine + + "Expected: [\"x.y\"]" + Environment.NewLine + + "Actual: [\"x.z\"]", + ex.Message + ); + } + } + + public class MismatchedMembers_Strict + { + [Fact] + public void Failure() + { + // Expected cannot be subset of Actual when strict is true + var ex = Record.Exception( + () => Assert.Equivalent( + new { x = 42 }, + new { x = 42, y = 2112 }, + strict: true + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched member list" + Environment.NewLine + + "Expected: [\"x\"]" + Environment.NewLine + + "Actual: [\"x\", \"y\"]", + ex.Message + ); + } + } + + public class ArrayOfValueTypes_NotStrict + { + [Fact] + public void Success() + { + Assert.Equivalent(new[] { 1, 4 }, new[] { 9, 4, 1 }, strict: false); + } + + [Fact] + public void Success_EmbeddedArray() + { + var expected = new { x = new[] { 1, 4 } }; + var actual = new { x = new[] { 9, 4, 1 } }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(new[] { 1, 6 }, new[] { 9, 4, 1 }, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray() + { + var expected = new { x = new[] { 1, 6 } }; + var actual = new { x = new[] { 9, 4, 1 } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + } + + public class ArrayOfValueTypes_Strict + { + [Fact] + public void Success() + { + Assert.Equivalent(new[] { 1, 9, 4 }, new[] { 9, 4, 1 }, strict: true); + } + + [Fact] + public void Success_EmbeddedArray() + { + var expected = new { x = new[] { 1, 9, 4 } }; + var actual = new { x = new[] { 9, 4, 1 } }; + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Failure_ValueNotFoundInActual() + { + var ex = Record.Exception(() => Assert.Equivalent(new[] { 1, 6 }, new[] { 9, 4, 1 }, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + + [Fact] + public void Failure_ExtraValueInActual() + { + var ex = Record.Exception(() => Assert.Equivalent(new[] { 1, 9, 4 }, new[] { 6, 9, 4, 1 }, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found" + Environment.NewLine + + "Expected: [1, 9, 4]" + Environment.NewLine + + "Actual: [6] left over from [6, 9, 4, 1]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray_ValueNotFoundInActual() + { + var expected = new { x = new[] { 1, 6 } }; + var actual = new { x = new[] { 9, 4, 1 } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray_ExtraValueInActual() + { + var expected = new { x = new[] { 1, 9, 4 } }; + var actual = new { x = new[] { 6, 9, 4, 1, 12 } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found in member 'x[]'" + Environment.NewLine + + "Expected: [1, 9, 4]" + Environment.NewLine + + "Actual: [6, 12] left over from [6, 9, 4, 1, 12]", + ex.Message + ); + } + } + + public class ImmutableArrayOfValueTypes_NotStrict + { + [Fact] + public void Success() + { + Assert.Equivalent(new[] { 1, 4 }.ToImmutableArray(), new[] { 9, 4, 1 }.ToImmutableArray(), strict: false); + } + + [Fact] + public void Success_EmbeddedArray() + { + var expected = new { x = new[] { 1, 4 }.ToImmutableArray() }; + var actual = new { x = new[] { 9, 4, 1 }.ToImmutableArray() }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Equivalent(new[] { 1, 6 }.ToImmutableArray(), new[] { 9, 4, 1 }.ToImmutableArray(), strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray() + { + var expected = new { x = new[] { 1, 6 }.ToImmutableArray() }; + var actual = new { x = new[] { 9, 4, 1 }.ToImmutableArray() }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + } + + public class ImmutableArrayOfValueTypes_Strict + { + [Fact] + public void Success() + { + Assert.Equivalent(new[] { 1, 9, 4 }.ToImmutableArray(), new[] { 9, 4, 1 }.ToImmutableArray(), strict: true); + } + + [Fact] + public void Success_EmbeddedArray() + { + var expected = new { x = new[] { 1, 9, 4 }.ToImmutableArray() }; + var actual = new { x = new[] { 9, 4, 1 }.ToImmutableArray() }; + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Failure_ValueNotFoundInActual() + { + var ex = Record.Exception(() => Assert.Equivalent(new[] { 1, 6 }.ToImmutableArray(), new[] { 9, 4, 1 }.ToImmutableArray(), strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + + [Fact] + public void Failure_ExtraValueInActual() + { + var ex = Record.Exception(() => Assert.Equivalent(new[] { 1, 9, 4 }.ToImmutableArray(), new[] { 6, 9, 4, 1 }.ToImmutableArray(), strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found" + Environment.NewLine + + "Expected: [1, 9, 4]" + Environment.NewLine + + "Actual: [6] left over from [6, 9, 4, 1]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray_ValueNotFoundInActual() + { + var expected = new { x = new[] { 1, 6 }.ToImmutableArray() }; + var actual = new { x = new[] { 9, 4, 1 }.ToImmutableArray() }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray_ExtraValueInActual() + { + var expected = new { x = new[] { 1, 9, 4 }.ToImmutableArray() }; + var actual = new { x = new[] { 6, 9, 4, 1, 12 }.ToImmutableArray() }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found in member 'x[]'" + Environment.NewLine + + "Expected: [1, 9, 4]" + Environment.NewLine + + "Actual: [6, 12] left over from [6, 9, 4, 1, 12]", + ex.Message + ); + } + } + + public class ArrayOfObjects_NotStrict + { + [Fact] + public void Success() + { + var expected = new[] { new { Foo = "Bar" } }; + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Success_EmbeddedArray() + { + var expected = new { x = new[] { new { Foo = "Bar" } } }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } } }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Failure() + { + var expected = new[] { new { Foo = "Biff" } }; + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: { Foo = \"Biff\" }" + Environment.NewLine + + "In: [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray() + { + var expected = new { x = new[] { new { Foo = "Biff" } } }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: { Foo = \"Biff\" }" + Environment.NewLine + + "In: [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + } + + public class ArrayOfObjects_Strict + { + [Fact] + public void Success() + { + var expected = new[] { new { Foo = "Bar" }, new { Foo = "Baz" } }; + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }; + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Success_EmbeddedArray() + { + var expected = new { x = new[] { new { Foo = "Bar" }, new { Foo = "Baz" } } }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } } }; + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Failure_ValueNotFoundInActual() + { + var expected = new[] { new { Foo = "Biff" } }; + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: { Foo = \"Biff\" }" + Environment.NewLine + + "In: [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + + [Fact] + public void Failure_ExtraValueInActual() + { + var expected = new[] { new { Foo = "Bar" } }; + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found" + Environment.NewLine + + "Expected: [{ Foo = \"Bar\" }]" + Environment.NewLine + + "Actual: [{ Foo = \"Baz\" }] left over from [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray_ValueNotFoundInActual() + { + var expected = new { x = new[] { new { Foo = "Biff" } } }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: { Foo = \"Biff\" }" + Environment.NewLine + + "In: [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray_ExtraValueInActual() + { + var expected = new { x = new[] { new { Foo = "Bar" } } }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found in member 'x[]'" + Environment.NewLine + + "Expected: [{ Foo = \"Bar\" }]" + Environment.NewLine + + "Actual: [{ Foo = \"Baz\" }] left over from [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + } + + public class ListOfObjects_NotStrict + { + [Fact] + public void Success() + { + var expected = new[] { new { Foo = "Bar" } }.ToList(); + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList(); + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Success_EmbeddedArray() + { + var expected = new { x = new[] { new { Foo = "Bar" } }.ToList() }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList() }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Failure() + { + var expected = new[] { new { Foo = "Biff" } }.ToList(); + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList(); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: { Foo = \"Biff\" }" + Environment.NewLine + + "In: [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray() + { + var expected = new { x = new[] { new { Foo = "Biff" } }.ToList() }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList() }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: { Foo = \"Biff\" }" + Environment.NewLine + + "In: [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + } + + public class ListOfObjects_Strict + { + [Fact] + public void Success() + { + var expected = new[] { new { Foo = "Bar" }, new { Foo = "Baz" } }.ToList(); + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList(); + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Success_EmbeddedList() + { + var expected = new { x = new[] { new { Foo = "Bar" }, new { Foo = "Baz" } }.ToList() }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList() }; + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Failure_ValueNotFoundInActual() + { + var expected = new[] { new { Foo = "Biff" } }.ToList(); + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList(); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: { Foo = \"Biff\" }" + Environment.NewLine + + "In: [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + + [Fact] + public void Failure_ExtraValueInActual() + { + var expected = new[] { new { Foo = "Bar" } }.ToList(); + var actual = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList(); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found" + Environment.NewLine + + "Expected: [{ Foo = \"Bar\" }]" + Environment.NewLine + + "Actual: [{ Foo = \"Baz\" }] left over from [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray_ValueNotFoundInActual() + { + var expected = new { x = new[] { new { Foo = "Biff" } }.ToList() }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList() }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: { Foo = \"Biff\" }" + Environment.NewLine + + "In: [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedArray_ExtraValueInActual() + { + var expected = new { x = new[] { new { Foo = "Bar" } }.ToList() }; + var actual = new { x = new[] { new { Foo = "Baz" }, new { Foo = "Bar" } }.ToList() }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found in member 'x[]'" + Environment.NewLine + + "Expected: [{ Foo = \"Bar\" }]" + Environment.NewLine + + "Actual: [{ Foo = \"Baz\" }] left over from [{ Foo = \"Baz\" }, { Foo = \"Bar\" }]", + ex.Message + ); + } + } + + public class EquivalentCollectionsInDifferentTypes + { + [Fact] + public void ArrayIsEquivalentToList() + { + Assert.Equivalent(new[] { 1, 2, 3 }, new List { 1, 2, 3 }); + } + + [Fact] + public void ListIsEquivalentToArray() + { + Assert.Equivalent(new List { 1, 2, 3 }, new[] { 1, 2, 3 }); + } + + [Fact] + public void ArrayIsEquivalentToImmutableArray() + { + Assert.Equivalent(new[] { 1, 2, 3 }, new[] { 1, 2, 3 }.ToImmutableArray()); + } + + [Fact] + public void ImmutableArrayIsEquivalentToArray() + { + Assert.Equivalent(new[] { 1, 2, 3 }.ToImmutableArray(), new[] { 1, 2, 3 }); + } + + [Fact] + public void ImmutableListIsEquivalentToImmutableSortedSet() + { + Assert.Equivalent(new[] { 1, 2, 3 }.ToImmutableList(), new[] { 1, 2, 3 }.ToImmutableSortedSet()); + } + } + + public class Dictionaries_NotStrict + { + [Fact] + public void Success() + { + var expected = new Dictionary { ["Foo"] = 42 }; + var actual = new Dictionary { ["Foo"] = 42, ["Bar"] = 2112 }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void SuccessWithArrayValues() + { + var expected = new Dictionary { ["Foo"] = [42] }; + var actual = new Dictionary { ["Foo"] = [42], ["Bar"] = [2112] }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void SuccessWithListValues() + { + var expected = new Dictionary> { ["Foo"] = [42] }; + var actual = new Dictionary> { ["Foo"] = [42], ["Bar"] = [2112] }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Success_EmbeddedDictionary() + { + var expected = new { x = new Dictionary { ["Foo"] = 42 } }; + var actual = new { x = new Dictionary { ["Foo"] = 42, ["Bar"] = 2112 } }; + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Failure() + { + var expected = new Dictionary { ["Foo"] = 16 }; + var actual = new Dictionary { ["Foo"] = 42, ["Bar"] = 2112 }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: [\"Foo\"] = 16" + Environment.NewLine + + "In: [[\"Foo\"] = 42, [\"Bar\"] = 2112]", + ex.Message + ); + } + + [Fact] + public void FailureWithArrayValues() + { + var expected = new Dictionary { ["Foo"] = [16] }; + var actual = new Dictionary { ["Foo"] = [42], ["Bar"] = [2112] }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: [\"Foo\"] = [16]" + Environment.NewLine + + "In: [[\"Foo\"] = [42], [\"Bar\"] = [2112]]", + ex.Message + ); + } + + [Fact] + public void FailureWithListValues() + { + var expected = new Dictionary> { ["Foo"] = [16] }; + var actual = new Dictionary> { ["Foo"] = [42], ["Bar"] = [2112] }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: [\"Foo\"] = [16]" + Environment.NewLine + + "In: [[\"Foo\"] = [42], [\"Bar\"] = [2112]]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedDictionary() + { + var expected = new { x = new Dictionary { ["Foo"] = 16 } }; + var actual = new { x = new Dictionary { ["Foo"] = 42, ["Bar"] = 2112 } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: [\"Foo\"] = 16" + Environment.NewLine + + "In: [[\"Foo\"] = 42, [\"Bar\"] = 2112]", + ex.Message + ); + } + } + + public class Dictionaries_Strict + { + [Fact] + public void Success() + { + var expected = new Dictionary { ["Bar"] = 2112, ["Foo"] = 42 }; + var actual = new Dictionary { ["Foo"] = 42, ["Bar"] = 2112 }; + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Success_EmbeddedDictionary() + { + var expected = new { x = new Dictionary { ["Bar"] = 2112, ["Foo"] = 42 } }; + var actual = new { x = new Dictionary { ["Foo"] = 42, ["Bar"] = 2112 } }; + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Failure_ValueNotFoundInActual() + { + var expected = new Dictionary { ["Foo"] = 16 }; + var actual = new Dictionary { ["Foo"] = 42, ["Bar"] = 2112 }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: [\"Foo\"] = 16" + Environment.NewLine + + "In: [[\"Foo\"] = 42, [\"Bar\"] = 2112]", + ex.Message + ); + } + + [Fact] + public void Failure_ExtraValueInActual() + { + var expected = new Dictionary { ["Bar"] = 2112, ["Foo"] = 42 }; + var actual = new Dictionary { ["Foo"] = 42, ["Biff"] = 2600, ["Bar"] = 2112 }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found" + Environment.NewLine + + "Expected: [[\"Bar\"] = 2112, [\"Foo\"] = 42]" + Environment.NewLine + + "Actual: [[\"Biff\"] = 2600] left over from [[\"Foo\"] = 42, [\"Biff\"] = 2600, [\"Bar\"] = 2112]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedDictionary_ValueNotFoundInActual() + { + var expected = new { x = new Dictionary { ["Foo"] = 16 } }; + var actual = new { x = new Dictionary { ["Foo"] = 42, ["Bar"] = 2112 } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'x[]'" + Environment.NewLine + + "Expected: [\"Foo\"] = 16" + Environment.NewLine + + "In: [[\"Foo\"] = 42, [\"Bar\"] = 2112]", + ex.Message + ); + } + + [Fact] + public void Failure_EmbeddedDictionary_ExtraValueInActual() + { + var expected = new { x = new Dictionary { ["Bar"] = 2112, ["Foo"] = 42 } }; + var actual = new { x = new Dictionary { ["Foo"] = 42, ["Biff"] = 2600, ["Bar"] = 2112 } }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found in member 'x[]'" + Environment.NewLine + + "Expected: [[\"Bar\"] = 2112, [\"Foo\"] = 42]" + Environment.NewLine + + "Actual: [[\"Biff\"] = 2600] left over from [[\"Foo\"] = 42, [\"Biff\"] = 2600, [\"Bar\"] = 2112]", + ex.Message + ); + } + } + + public class KeyValuePairs_NotStrict + { + [Fact] + public void Success() + { + var expected = new KeyValuePair(42, [1, 4]); + var actual = new KeyValuePair(42, [9, 4, 1]); + + Assert.Equivalent(expected, actual, strict: false); + } + + [Fact] + public void Failure_Key() + { + var expected = new KeyValuePair(42, [1, 4]); + var actual = new KeyValuePair(41, [9, 4, 1]); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Key'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 41", + ex.Message + ); + } + + [Fact] + public void Failure_Value() + { + var expected = new KeyValuePair(42, [1, 6]); + var actual = new KeyValuePair(42, [9, 4, 1]); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: false)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'Value[]'" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [9, 4, 1]", + ex.Message + ); + } + } + + public class KeyValuePairs_Strict + { + [Fact] + public void Success() + { + var expected = new KeyValuePair(42, [1, 4]); + var actual = new KeyValuePair(42, [4, 1]); + + Assert.Equivalent(expected, actual, strict: true); + } + + [Fact] + public void Failure_Key() + { + var expected = new KeyValuePair(42, [1, 4]); + var actual = new KeyValuePair(41, [4, 1]); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Key'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 41", + ex.Message + ); + } + + [Fact] + public void Failure_Value() + { + var expected = new KeyValuePair(42, [1, 6]); + var actual = new KeyValuePair(42, [4, 1]); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual, strict: true)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found in member 'Value[]'" + Environment.NewLine + + "Expected: 6" + Environment.NewLine + + "In: [4, 1]", + ex.Message + ); + } + } + + // https://github.com/xunit/xunit/issues/3028 + public class Groupings + { + [Fact] + public static void Success() + { + var expected = Enumerable.Range(1, 4).ToLookup(i => (i % 2) == 0); + var actual = Enumerable.Range(1, 4).GroupBy(i => (i % 2) == 0); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public static void Failure_KeysDontMatch() + { + var expected = Enumerable.Range(1, 4).ToLookup(i => true); + var actual = Enumerable.Range(1, 4).GroupBy(i => false); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Collection value not found" + Environment.NewLine + + "Expected: [True] = [1, 2, 3, 4]" + Environment.NewLine + + "In: [[False] = [1, 2, 3, 4]]", + ex.Message + ); + } + + [Fact] + public static void Failure_KeysMatch_ValuesDoNot() + { + var expected = Enumerable.Range(1, 4).ToLookup(i => (i % 2) == 0); + var actual = Enumerable.Range(1, 4).GroupBy(i => (i % 2) == 1); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Grouping key [False] has mismatched values" + Environment.NewLine + + "Expected: [1, 3]" + Environment.NewLine + + "Actual: [2, 4]", + ex.Message + ); + } + } + + public class SpecialCases + { + // DateTime + + [Fact] + public void DateTime_Success() + { + var expected = new DateTime(2022, 12, 1, 1, 3, 1); + var actual = new DateTime(2022, 12, 1, 1, 3, 1); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void DateTime_Failure() + { + var expected = new DateTime(2022, 12, 1, 1, 3, 1); + var actual = new DateTime(2011, 9, 13, 18, 22, 0); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: 2022-12-01T01:03:01.0000000" + Environment.NewLine + + "Actual: 2011-09-13T18:22:00.0000000", + ex.Message + ); + } + + [Fact] + public void DateTimeToString_Failure() + { + var expected = new DateTime(2022, 12, 1, 1, 3, 1); + var actual = "2022-12-01T01:03:01.0000000"; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: 2022-12-01T01:03:01.0000000" + Environment.NewLine + + "Actual: \"2022-12-01T01:03:01.0000000\"", + ex.Message + ); + Assert.IsType(ex.InnerException); // Thrown by DateTime.CompareTo + } + + [Fact] + public void StringToDateTime_Success() + { + var expected = "2022-12-01T01:03:01.0000000"; + var actual = new DateTime(2022, 12, 1, 1, 3, 1); + + Assert.Equivalent(expected, actual); + } + + // DateTimeOffset + + [Fact] + public void DateTimeOffset_Success() + { + var expected = new DateTimeOffset(2022, 12, 1, 1, 3, 1, TimeSpan.Zero); + var actual = new DateTimeOffset(2022, 12, 1, 1, 3, 1, TimeSpan.Zero); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void DateTimeOffset_Failure() + { + var expected = new DateTimeOffset(2022, 12, 1, 1, 3, 1, TimeSpan.Zero); + var actual = new DateTimeOffset(2011, 9, 13, 18, 22, 0, TimeSpan.Zero); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: 2022-12-01T01:03:01.0000000+00:00" + Environment.NewLine + + "Actual: 2011-09-13T18:22:00.0000000+00:00", + ex.Message + ); + } + + // FileSystemInfo-derived types + + [Fact] + public void DirectoryInfo_Success() + { + var assemblyPath = Path.GetDirectoryName(typeof(SpecialCases).Assembly.Location); + Assert.NotNull(assemblyPath); + + var expected = new DirectoryInfo(assemblyPath); + var actual = new DirectoryInfo(assemblyPath); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void DirectoryInfo_Failure() + { + var assemblyPath = Path.GetDirectoryName(typeof(SpecialCases).Assembly.Location); + Assert.NotNull(assemblyPath); + var assemblyParentPath = Path.GetDirectoryName(assemblyPath); + Assert.NotNull(assemblyParentPath); + Assert.NotEqual(assemblyPath, assemblyParentPath); + + var expected = new FileInfo(assemblyPath); + var actual = new FileInfo(assemblyParentPath); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.StartsWith("Assert.Equivalent() Failure: Mismatched value on member 'FullName'" + Environment.NewLine, ex.Message); + } + + [Fact] + public void FileInfo_Success() + { + var assembly = typeof(SpecialCases).Assembly.Location; + var expected = new FileInfo(assembly); + var actual = new FileInfo(assembly); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void FileInfo_Failure() + { + var expected = new FileInfo(typeof(SpecialCases).Assembly.Location); + var actual = new FileInfo(typeof(Assert).Assembly.Location); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.StartsWith("Assert.Equivalent() Failure: Mismatched value on member 'FullName'" + Environment.NewLine, ex.Message); + } + + [Fact] + public void FileInfoToDirectoryInfo_Failure_TopLevel() + { + var location = typeof(SpecialCases).Assembly.Location; + var expected = new FileInfo(location); + var actual = new DirectoryInfo(location); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Types did not match" + Environment.NewLine + + "Expected type: System.IO.FileInfo" + Environment.NewLine + + "Actual type: System.IO.DirectoryInfo", + ex.Message + ); + } + + [Fact] + public void FileInfoToDirectoryInfo_Failure_Embedded() + { + var location = typeof(SpecialCases).Assembly.Location; + var expected = new { Info = new FileInfo(location) }; + var actual = new { Info = new DirectoryInfo(location) }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Types did not match in member 'Info'" + Environment.NewLine + + "Expected type: System.IO.FileInfo" + Environment.NewLine + + "Actual type: System.IO.DirectoryInfo", + ex.Message + ); + } + + // Uri + + public static TheoryData UriData = + [ + new Uri("https://xunit.net/"), + new Uri("a/b#c", UriKind.RelativeOrAbsolute), + ]; + + [Theory] + [MemberData(nameof(UriData))] + public void Uri_Success(Uri uri) + { + Assert.Equivalent(uri, new Uri(uri.OriginalString, UriKind.RelativeOrAbsolute)); + } + + [Theory] + [MemberData(nameof(UriData))] + public void Uri_Failure(Uri uri) + { + var ex = Record.Exception(() => Assert.Equivalent(uri, new Uri("hello/world", UriKind.RelativeOrAbsolute))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure" + Environment.NewLine + + "Expected: " + uri.OriginalString + Environment.NewLine + + "Actual: hello/world", + ex.Message + ); + } + + // Ensuring we use reference equality for the circular reference hash sets + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Issue2939(bool strict) + { + var expected = new Uri("http://example.com"); + var actual = new Uri("http://example.com"); + + Assert.Equivalent(expected, actual, strict); + } + + // Lazy + + [Fact] + public void LazyValueEquivalentToValue() + { + var expected = "Hello"; + var actual = new Lazy(() => "Hello"); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void ValueEquivalentToLazyValue() + { + var expected = new Lazy(() => "Hello"); + var actual = "Hello"; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void UnretrievedLazyValueEquivalentToRetrievedLazyValue() + { + var expected = new Lazy(() => "Hello"); + var actual = new Lazy(() => "Hello"); + _ = expected.Value; + + Assert.Equivalent(expected, actual); + } + } + + public class Obsolete + { + [Fact] + public void SkipsObsoleteProperties() + { + var value1 = new ClassWithObsoleteProperty { Value = 42 }; + var value2 = new ClassWithObsoleteProperty { Value = 42 }; + + Assert.Equivalent(value1, value2); + } + + class ClassWithObsoleteProperty + { + public int Value { get; set; } + + [Obsolete("This property is obsolete")] + public int ObsoleteProperty => throw new NotImplementedException(); + } + + [Fact] + public void SkipsObsoletePropertyGetters() + { + var value1 = new ClassWithObsoletePropertyGetter { Value = 42, ObsoleteProperty = 2112 }; + var value2 = new ClassWithObsoletePropertyGetter { Value = 42, ObsoleteProperty = 2600 }; + + Assert.Equivalent(value1, value2); + } + + class ClassWithObsoletePropertyGetter + { + public int Value { get; set; } + + public int ObsoleteProperty + { + [Obsolete("This property is obsolete")] + get { throw new NotImplementedException(); } + set { } + } + } + } + + public class CircularReferences + { + [Fact] + public void Expected_Shallow() + { + var expected = new SelfReferential(circularReference: true); + var actual = new SelfReferential(circularReference: false); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal("Assert.Equivalent() Failure: Circular reference found in 'expected.Other'", ex.Message); + } + + [Fact] + public void Actual_Shallow() + { + var expected = new SelfReferential(circularReference: false); + var actual = new SelfReferential(circularReference: true); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal("Assert.Equivalent() Failure: Circular reference found in 'actual.Other'", ex.Message); + } + } + + public class DepthLimit + { + [Fact] + public void PreventArbitrarilyLargeDepthObjectTree() + { + var expected = new InfiniteRecursionClass(); + var actual = new InfiniteRecursionClass(); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.Equivalent() Failure: Exceeded the maximum depth {EnvironmentVariables.Defaults.AssertEquivalentMaxDepth} with '{string.Join(".", Enumerable.Repeat("Parent", EnvironmentVariables.Defaults.AssertEquivalentMaxDepth))}'; check for infinite recursion or circular references", + ex.Message + ); + } + + class InfiniteRecursionClass + { +#pragma warning disable CA1822 // Mark members as static + public InfiniteRecursionClass Parent => new(); +#pragma warning restore CA1822 // Mark members as static + } + } + + public class Indexers + { + [Fact] + public void Equivalent() + { + var expected = new ClassWithIndexer { Value = "Hello" }; + var actual = new ClassWithIndexer { Value = "Hello" }; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void NotEquivalent() + { + var expected = new ClassWithIndexer { Value = "Hello" }; + var actual = new ClassWithIndexer { Value = "There" }; + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Value'" + Environment.NewLine + + "Expected: \"Hello\"" + Environment.NewLine + + "Actual: \"There\"", + ex.Message + ); + } + } + + public class Tuples + { + [Fact] + public void Equivalent() + { + var expected = Tuple.Create(42, "Hello world"); + var actual = Tuple.Create(42, "Hello world"); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void NotEquivalent() + { + var expected = Tuple.Create(42, "Hello world"); + var actual = Tuple.Create(2112, "Hello world"); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Item1'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + } + + public class ValueTuples + { + [Fact] + public void Equivalent() + { + var expected = (answer: 42, greeting: "Hello world"); + var actual = (answer: 42, greeting: "Hello world"); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void NotEquivalent() + { + var expected = (answer: 42, greeting: "Hello world"); + var actual = (answer: 2112, greeting: "Hello world"); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Item1'" + Environment.NewLine + + "Expected: 42" + Environment.NewLine + + "Actual: 2112", + ex.Message + ); + } + + [Fact] + public void ValueTupleInsideClass_Equivalent() + { + var expected = new Person { ID = 42, Relationships = (parent: new Person { ID = 2112 }, child: null) }; + var actual = new Person { ID = 42, Relationships = (parent: new Person { ID = 2112 }, child: null) }; + + Assert.Equivalent(expected, actual); + } + + class Person + { + public int ID { get; set; } + + public (Person? parent, Person? child) Relationships; + } + } + + public class WithExclusions + { + public class ByExpression + { + [Fact] + public void Shallow() + { + Assert.EquivalentWithExclusions( + new ShallowClass { Value1 = 42, Value2 = "Hello" }, + new ShallowClass { Value1 = 42, Value2 = "World" }, + s => s.Value2 + ); + } + + [Fact] + public void MixedShallowAndDeep() + { + Assert.EquivalentWithExclusions( + new DeepClass { Value3 = 21.12m, Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello" } }, + new DeepClass { Value3 = 42.24m, Shallow = new ShallowClass { Value1 = 42, Value2 = "World" } }, + d => d.Value3, + d => d.Shallow!.Value2 + ); + } + + // https://github.com/xunit/xunit/issues/3338 + // https://github.com/xunit/xunit/issues/3347 + [Fact] + public void PartialDeepComparisonBug() + { + var ex = Record.Exception(() => + Assert.EquivalentWithExclusions( + new DeepClass { Value3 = 21.12m, Shallow = new ShallowClass { Value1 = 4, Value2 = "Hello" }, Other = new ShallowClass { Value1 = 10, Value2 = "world" } }, + new DeepClass { Value3 = 42.24m, Shallow = new ShallowClass { Value1 = 2, Value2 = "Hello" }, Other = new ShallowClass { Value1 = 15, Value2 = "world" } }, + d => d.Value3, + d => d.Shallow!.Value2, + d => d.Other!.Value1 + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Shallow.Value1'" + Environment.NewLine + + "Expected: 4" + Environment.NewLine + + "Actual: 2", + ex.Message + ); + } + } + + public class ByString + { + [Fact] + public void Shallow() + { + Assert.EquivalentWithExclusions( + new ShallowClass { Value1 = 42, Value2 = "Hello" }, + new ShallowClass { Value1 = 42, Value2 = "World" }, + "Value2" + ); + } + + [Fact] + public void MixedShallowAndDeep() + { + Assert.EquivalentWithExclusions( + new DeepClass { Value3 = 21.12m, Shallow = new ShallowClass { Value1 = 42, Value2 = "Hello" } }, + new DeepClass { Value3 = 42.24m, Shallow = new ShallowClass { Value1 = 42, Value2 = "World" } }, + "Value3", + "Shallow.Value2" + ); + } + + // https://github.com/xunit/xunit/issues/3338 + // https://github.com/xunit/xunit/issues/3347 + [Fact] + public void PartialDeepComparisonBug() + { + var ex = Record.Exception(() => + Assert.EquivalentWithExclusions( + new DeepClass { Value3 = 21.12m, Shallow = new ShallowClass { Value1 = 4, Value2 = "Hello" }, Other = new ShallowClass { Value1 = 10, Value2 = "world" } }, + new DeepClass { Value3 = 42.24m, Shallow = new ShallowClass { Value1 = 2, Value2 = "Hello" }, Other = new ShallowClass { Value1 = 15, Value2 = "world" } }, + "Value3", + "Shallow.Value2", + "Other.Value1" + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Shallow.Value1'" + Environment.NewLine + + "Expected: 4" + Environment.NewLine + + "Actual: 2", + ex.Message + ); + } + + // https://github.com/xunit/xunit/issues/3394 + [Fact] + public void Collections() + { + var expected = new DeepRecord() + { + Value1 = 35, + Shallow = [ + new() { Value2 = 42, Value3 = 10 }, + ], + }; + var actual = new DeepRecord() + { + Value1 = 35, + Shallow = [ + new() { Value2 = 42, Value3 = 15 }, + new() { Value2 = 50, Value3 = 3 }, + ], + }; + + Assert.EquivalentWithExclusions(expected, actual, strict: false, "Shallow[].Value3"); + } + + // https://github.com/xunit/xunit/issues/3394 + [Fact] + public void Collections_Strict() + { + var expected = new DeepRecord() + { + Value1 = 35, + Shallow = [ + new() { Value2 = 42, Value3 = 10 }, + ], + }; + var actual = new DeepRecord() + { + Value1 = 35, + Shallow = [ + new() { Value2 = 42, Value3 = 15 }, + new() { Value2 = 50, Value3 = 3 }, + ], + }; + + var ex = Record.Exception(() => Assert.EquivalentWithExclusions(expected, actual, strict: true, "Shallow[].Value3")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Extra values found in member 'Shallow[]'" + Environment.NewLine + + "Expected: [ShallowRecord { Value2 = 42, Value3 = 10 }]" + Environment.NewLine + + "Actual: [ShallowRecord { Value2 = 50, Value3 = 3 }] left over from [ShallowRecord { Value2 = 42, Value3 = 15 }, ShallowRecord { Value2 = 50, Value3 = 3 }]", + ex.Message + ); + } + + class DeepRecord + { + public required int Value1 { get; set; } + public required List Shallow { get; set; } + } + + class ShallowRecord + { + public required int Value2 { get; set; } + public required int Value3 { get; set; } + } + } + } + +#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + + // https://github.com/xunit/xunit/issues/3088 + public sealed class ByRefLikeParameters + { + [Fact] + public void Equivalent() + { + var expected = new RefParameterClass(42.ToString()); + var actual = new RefParameterClass(42.ToString()); + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void NotEquivalent() + { + var expected = new RefParameterClass2(42.ToString()); + var actual = new RefParameterClass2(2112.ToString()); + + var ex = Record.Exception(() => Assert.Equivalent(expected, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equivalent() Failure: Mismatched value on member 'Value'" + Environment.NewLine + + "Expected: \"42\"" + Environment.NewLine + + "Actual: \"2112\"", + ex.Message + ); + } + + public sealed class RefParameterClass(string value) + { + public ReadOnlySpan Value => value.AsSpan(); + } + + public sealed class RefParameterClass2(string value) + { + public ReadOnlySpan ValueSpan => value.AsSpan(); + + // Added so `NotEquivalent` has a getter to check. + public string Value => value; + } + } + +#endif + + public sealed class ClassWithNewOverrides + { + [Fact] + public void ExpectedOverridden() + { + var expected = new DerivedClass { ID = "123" }; + var actual = new BaseClass { ID = "123" }; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void ActualOverridden() + { + var expected = new BaseClass { ID = "123" }; + var actual = new DerivedClass { ID = "123" }; + + Assert.Equivalent(expected, actual); + } + + [Fact] + public void BothPropertiesOverridden() + { + var expected = new DerivedClass { ID = "123" }; + var actual = new DerivedClass { ID = "123" }; + + Assert.Equivalent(expected, actual); + } + + public class BaseClass + { + public object? ID { get; set; } + } + + public class DerivedClass : BaseClass + { + public new string? ID { get; set; } + } + } + + class ShallowClass + { + public static int StaticValue { get; set; } + public int Value1; + public string? Value2 { get; set; } + } + + class ShallowClass2 + { + public static int StaticValue { get; set; } + public int Value1 { get; set; } + public string? Value2; + } + + class PrivateMembersClass(int value1, string value2) + { + readonly int Value1 = value1; +#pragma warning disable IDE0051 + string Value2 { get; } = value2; +#pragma warning restore IDE0051 + } + + class DeepClass + { + public decimal Value3; + public ShallowClass? Shallow { get; set; } + public ShallowClass? Other { get; set; } + } + + class DeepClass2 + { + public decimal Value3 { get; set; } + public ShallowClass? Shallow; +#pragma warning disable CA1822 // Mark members as static + public ShallowClass? Other => null; +#pragma warning restore CA1822 // Mark members as static + } + + readonly struct DeepStruct(ShallowClass shallow) + { + public ShallowClass Shallow { get; } = shallow; + } + + class SelfReferential + { + public SelfReferential(bool circularReference) + { + // When we don't want this object to be self-referential, we need to make *another* + // object here instead, since a null value would end up short circuiting the + // circular reference check before we saw it. We don't anticipate people having + // to do strange things like this; it's just for testing. :) + Other = circularReference ? this : new SelfReferential(true); + } + + public SelfReferential Other { get; } + } + + class ClassWithIndexer + { + public string? Value; + + public string this[int idx] => idx.ToString(); + } +} + +#endif diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EventAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EventAssertsTests.cs new file mode 100644 index 00000000000..5b49591dae7 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/EventAssertsTests.cs @@ -0,0 +1,956 @@ +using Xunit; +using Xunit.Sdk; + +public class EventAssertsTests +{ + public class Raises_Action + { + [Fact] + public static void NoEventRaised() + { + var obj = new RaisingClass_ActionOfT(); + + var ex = Record.Exception( + () => Assert.Raises( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Raises() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static void NoEventRaised_NoData() + { + var obj = new RaisingClass_Action(); + + var ex = Record.Exception( + () => Assert.Raises( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.IsType(ex); + Assert.Equal("Assert.Raises() Failure: No event was raised", ex.Message); + } + + [Fact] + public static void ExactTypeRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new object(); + + var evt = Assert.Raises( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Null(evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static void RaisingClass_ActionOfT() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new DerivedObject(); + + var ex = Record.Exception( + () => Assert.Raises( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Raises() Failure: Wrong event type was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + $"Actual: typeof({typeof(DerivedObject).FullName})", + ex.Message + ); + } + } + + public class Raises_EventHandler + { + [Fact] + public static void NoEventRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + + var ex = Record.Exception( + () => Assert.Raises( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Raises() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static void ExactTypeRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new object(); + + var evt = Assert.Raises( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static void DerivedTypeRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new DerivedObject(); + + var ex = Record.Exception( + () => Assert.Raises( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Raises() Failure: Wrong event type was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + $"Actual: typeof({typeof(DerivedObject).FullName})", + ex.Message + ); + } + + [Fact] + public static void CustomRaised() + { + var obj = new RaisingClass_CustomEventHandler(); + var eventObj = new object(); + Assert.RaisedEvent? raisedEvent = null; + void handler(object? s, object args) => raisedEvent = new Assert.RaisedEvent(s, args); + + var evt = Assert.Raises( + () => raisedEvent, + () => obj.Completed += handler, + () => obj.Completed -= handler, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + } + + public class RaisesAny_Action + { + [Fact] + public static void NoEventRaised() + { + var obj = new RaisingClass_ActionOfT(); + + var ex = Record.Exception( + () => Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.RaisesAny() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static void ExactTypeRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new object(); + + var evt = Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Null(evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static void DerivedTypeRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new DerivedObject(); + + var evt = Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Null(evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + } + + public class RaisesAny_EventHandler + { + [Fact] + public static void NoEventRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + + var ex = Record.Exception( + () => Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.RaisesAny() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static void NoEventRaised_NonGeneric() + { + var obj = new RaisingClass_EventHandler(); + + var ex = Record.Exception( + () => Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.RaisesAny() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(System.EventArgs)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static void ExactTypeRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new object(); + + var evt = Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static void ExactTypeRaised_NonGeneric() + { + var obj = new RaisingClass_EventHandler(); + var eventObj = new EventArgs(); + + var evt = Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static void DerivedTypeRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new DerivedObject(); + + var evt = Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static void DerivedTypeRaised_NonGeneric() + { + var obj = new RaisingClass_EventHandler(); + var eventObj = new DerivedEventArgs(); + + var evt = Assert.RaisesAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + } + + public class RaisesAnyAsync_Action + { + [Fact] + public static async Task NoEventRaised() + { + var obj = new RaisingClass_ActionOfT(); + + var ex = await Record.ExceptionAsync( + () => Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.FromResult(0) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.RaisesAny() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static async Task ExactTypeRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new object(); + + var evt = await Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ); + + Assert.NotNull(evt); + Assert.Null(evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static async Task DerivedTypeRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new DerivedObject(); + + var evt = await Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ); + + Assert.NotNull(evt); + Assert.Null(evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + } + + public class RaisesAnyAsync_EventHandler + { + [Fact] + public static async Task NoEventRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + + var ex = await Record.ExceptionAsync( + () => Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.FromResult(0) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.RaisesAny() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static async Task NoEventRaised_NonGeneric() + { + var obj = new RaisingClass_EventHandler(); + + var ex = await Record.ExceptionAsync( + () => Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.FromResult(0) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.RaisesAny() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(System.EventArgs)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static async Task ExactTypeRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new object(); + + var evt = await Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static async Task ExactTypeRaised_NonGeneric() + { + var obj = new RaisingClass_EventHandler(); + var eventObj = new EventArgs(); + + var evt = await Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static async Task DerivedTypeRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new DerivedObject(); + + var evt = await Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static async Task DerivedTypeRaised_NonGeneric() + { + var obj = new RaisingClass_EventHandler(); + var eventObj = new DerivedEventArgs(); + + var evt = await Assert.RaisesAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + } + + public class RaisesAsync_Action + { + [Fact] + public static async Task NoEventRaised() + { + var obj = new RaisingClass_ActionOfT(); + + var ex = await Record.ExceptionAsync( + () => Assert.RaisesAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.FromResult(0) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Raises() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static async Task NoEventRaised_NoData() + { + var obj = new RaisingClass_Action(); + + var ex = await Record.ExceptionAsync( + () => Assert.RaisesAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.CompletedTask + ) + ); + + Assert.IsType(ex); + Assert.Equal("Assert.Raises() Failure: No event was raised", ex.Message); + } + + [Fact] + public static async Task ExactTypeRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new object(); + + var evt = await Assert.RaisesAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ); + + Assert.NotNull(evt); + Assert.Null(evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static async Task DerivedTypeRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new DerivedObject(); + + var ex = await Record.ExceptionAsync( + () => Assert.RaisesAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Raises() Failure: Wrong event type was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + $"Actual: typeof({typeof(DerivedObject).FullName})", + ex.Message + ); + } + } + + public class RaisesAsync_EventHandler + { + [Fact] + public static async Task NoEventRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + + var ex = await Record.ExceptionAsync( + () => Assert.RaisesAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.FromResult(0) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Raises() Failure: No event was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: No event was raised", + ex.Message + ); + } + + [Fact] + public static async Task ExactTypeRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new object(); + + var evt = await Assert.RaisesAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ); + + Assert.NotNull(evt); + Assert.Equal(obj, evt.Sender); + Assert.Equal(eventObj, evt.Arguments); + } + + [Fact] + public static async Task DerivedTypeRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new DerivedObject(); + + var ex = await Record.ExceptionAsync( + () => Assert.RaisesAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Raises() Failure: Wrong event type was raised" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + $"Actual: typeof({typeof(DerivedObject).FullName})", + ex.Message + ); + } + } + + public class NotRaisedAny_Action + { + [Fact] + public static void NoEventRaised() + { + var obj = new RaisingClass_ActionOfT(); + + var ex = Record.Exception( + () => Assert.NotRaisedAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.Null(ex); + } + + [Fact] + public static void EventRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new object(); + + var ex = Record.Exception( + () => Assert.NotRaisedAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotRaisedAny() Failure: An unexpected event was raised{Environment.NewLine}Unexpected: typeof(object){Environment.NewLine}Actual: An event was raised", + ex.Message + ); + } + + } + + public class NotRaisedAny_EventHandler + { + [Fact] + public static void NoEventRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + + var ex = Record.Exception( + () => Assert.NotRaisedAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.Null(ex); + } + + [Fact] + public static void EventRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new object(); + + var ex = Record.Exception( + () => Assert.NotRaisedAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.RaiseWithArgs(eventObj) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotRaisedAny() Failure: An unexpected event was raised{Environment.NewLine}Unexpected: typeof(object){Environment.NewLine}Actual: An event was raised", + ex.Message + ); + } + } + + public class NotRaisedAnyAsync_Action + { + [Fact] + public static async Task NoEventRaised() + { + var obj = new RaisingClass_ActionOfT(); + + var ex = await Record.ExceptionAsync( + () => Assert.NotRaisedAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.FromResult(0) + ) + ); + + Assert.Null(ex); + } + + [Fact] + public static async Task EventRaised() + { + var obj = new RaisingClass_ActionOfT(); + var eventObj = new object(); + + var ex = await Record.ExceptionAsync( + () => Assert.NotRaisedAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotRaisedAny() Failure: An unexpected event was raised{Environment.NewLine}Unexpected: typeof(object){Environment.NewLine}Actual: An event was raised", + ex.Message + ); + } + } + + public class NotRaisedAnyAsync_EventHandler + { + [Fact] + public static async Task NoEventRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + + var ex = await Record.ExceptionAsync( + () => Assert.NotRaisedAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.FromResult(0) + ) + ); + + Assert.Null(ex); + } + + [Fact] + public static async Task EventRaised() + { + var obj = new RaisingClass_EventHandlerOfT(); + var eventObj = new object(); + + var ex = await Record.ExceptionAsync( + () => Assert.NotRaisedAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.RaiseWithArgs(eventObj), TestContext.Current.CancellationToken) + ) + ); + + Assert.IsType(ex); + Assert.Equal( + $"Assert.NotRaisedAny() Failure: An unexpected event was raised{Environment.NewLine}Unexpected: typeof(object){Environment.NewLine}Actual: An event was raised", + ex.Message + ); + } + } + + public class NotRaisedAny_NoArgs + { + [Fact] + public static void NoEventRaised() + { + var obj = new RaisingClass_Action(); + + var ex = Record.Exception( + () => Assert.NotRaisedAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => { } + ) + ); + + Assert.Null(ex); + } + + [Fact] + public static void EventRaised() + { + var obj = new RaisingClass_Action(); + + var ex = Record.Exception( + () => Assert.NotRaisedAny( + h => obj.Completed += h, + h => obj.Completed -= h, + () => obj.Raise() + ) + ); + + Assert.IsType(ex); + Assert.Equal("Assert.NotRaisedAny() Failure: An unexpected event was raised", ex.Message); + } + } + + public class NotRaisedAnyAsync_NoArgs + { + [Fact] + public static async Task NoEventRaised() + { + var obj = new RaisingClass_Action(); + + var ex = await Record.ExceptionAsync( + () => Assert.NotRaisedAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.CompletedTask + ) + ); + + Assert.Null(ex); + } + + [Fact] + public static async Task EventRaised() + { + var obj = new RaisingClass_Action(); + + var ex = await Record.ExceptionAsync( + () => Assert.NotRaisedAnyAsync( + h => obj.Completed += h, + h => obj.Completed -= h, + () => Task.Run(() => obj.Raise(), TestContext.Current.CancellationToken) + ) + ); + + Assert.IsType(ex); + Assert.Equal("Assert.NotRaisedAny() Failure: An unexpected event was raised", ex.Message); + } + } + + class RaisingClass_Action + { + public void Raise() + { + Completed!.Invoke(); + } + + public event Action? Completed; + } + + class RaisingClass_ActionOfT + { + public void RaiseWithArgs(object args) + { + Completed!.Invoke(args); + } + + public event Action? Completed; + } + + class RaisingClass_EventHandler + { + public void RaiseWithArgs(EventArgs args) + { + Completed!.Invoke(this, args); + } + + public event EventHandler? Completed; + } + + class RaisingClass_EventHandlerOfT + { + public void RaiseWithArgs(object args) + { + Completed!.Invoke(this, args); + } + + public event EventHandler? Completed; + } + + class RaisingClass_CustomEventHandler + { + public void RaiseWithArgs(object args) + { + Completed!.Invoke(this, args); + } + + public event CustomEventHandler? Completed; + } + + class DerivedEventArgs : EventArgs { } + + class DerivedObject : object { } + + delegate void CustomEventHandler(object sender, TEventArgs e); +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/ExceptionAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/ExceptionAssertsTests.cs new file mode 100644 index 00000000000..78b82e22543 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/ExceptionAssertsTests.cs @@ -0,0 +1,1947 @@ +using Xunit; +using Xunit.Sdk; +using Xunit.v3; + +public class ExceptionAssertsTests +{ +#pragma warning disable xUnit2015 // Do not use typeof expression to check the exception type + + public class Throws_NonGeneric + { + public class WithAction + { + [Fact] + public static void GuardClauses() + { + static void testCode() { } + + Assert.Throws("exceptionType", () => Assert.Throws(null!, testCode)); + Assert.Throws("testCode", () => Assert.Throws(typeof(Exception), default(Action)!)); + } + + [Fact] + public static void NoExceptionThrown() + { + static void testCode() { } + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown() + { + static void testCode() => throw new ArgumentException(); + + Assert.Throws(typeof(ArgumentException), testCode); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static void testCode() => Assert.Skip("This is a skipped test"); + + try + { + Assert.Throws(typeof(Exception), testCode); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithActionAndInspector + { + [Fact] + public static void GuardClauses() + { + static void testCode() { } + + Assert.Throws("exceptionType", () => Assert.Throws(null!, testCode, _ => null)); + Assert.Throws("testCode", () => Assert.Throws(typeof(Exception), default(Action)!, _ => null)); + } + + [Fact] + public static void NoExceptionThrown() + { + static void testCode() { } + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorPasses() + { + static void testCode() => throw new ArgumentException(); + + Assert.Throws(typeof(ArgumentException), testCode, _ => null); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorFails() + { + static void testCode() => throw new ArgumentException(); + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.Throws() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorThrows() + { + static void testCode() => throw new ArgumentException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.StartsWith( + "Assert.Throws() Failure: Exception thrown by inspector", + ex.Message + ); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static void testCode() => Assert.Skip("This is a skipped test"); + + try + { + Assert.Throws(typeof(Exception), testCode, _ => null); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithFunc + { + [Fact] + public static void GuardClauses() + { + static object testCode() => 42; + + Assert.Throws("exceptionType", () => Assert.Throws(null!, testCode)); + Assert.Throws("testCode", () => Assert.Throws(typeof(Exception), default(Func)!)); + } + + [Fact] + public static void ProtectsAgainstAccidentalTask() + { + static object testCode() => Task.FromResult(42); + + var ex = Record.Exception(() => Assert.Throws(typeof(Exception), testCode)); + + Assert.IsType(ex); + Assert.Equal("You must call Assert.ThrowsAsync when testing async code", ex.Message); + } + + [Fact] + public static void NoExceptionThrown() + { + static object testCode() => 42; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown() + { + static object testCode() => throw new ArgumentException(); + + Assert.Throws(typeof(ArgumentException), testCode); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static object testCode() { Assert.Skip("This is a skipped test"); return null; } + + try + { + Assert.Throws(typeof(Exception), testCode); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithFuncAndInspector + { + [Fact] + public static void GuardClauses() + { + static object testCode() => 42; + + Assert.Throws("exceptionType", () => Assert.Throws(null!, testCode, _ => null)); + Assert.Throws("testCode", () => Assert.Throws(typeof(Exception), default(Func)!, _ => null)); + } + + [Fact] + public static void ProtectsAgainstAccidentalTask() + { + static object testCode() => Task.FromResult(42); + + var ex = Record.Exception(() => Assert.Throws(typeof(Exception), testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal("You must call Assert.ThrowsAsync when testing async code", ex.Message); + } + + [Fact] + public static void NoExceptionThrown() + { + static object testCode() => 42; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorPasses() + { + static object testCode() => throw new ArgumentException(); + + Assert.Throws(typeof(ArgumentException), testCode, _ => null); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorFails() + { + static object testCode() => throw new ArgumentException(); + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.Throws() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorThrows() + { + static object testCode() => throw new ArgumentException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.StartsWith( + "Assert.Throws() Failure: Exception thrown by inspector", + ex.Message + ); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(typeof(ArgumentException), testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static object testCode() { Assert.Skip("This is a skipped test"); return null; } + + try + { + Assert.Throws(typeof(Exception), testCode, _ => null); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + } + +#pragma warning restore xUnit2015 // Do not use typeof expression to check the exception type + + public class Throws_Generic + { + public class WithAction + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.Throws(default(Action)!)); + } + + [Fact] + public static void NoExceptionThrown() + { + static void testCode() { } + + var ex = Record.Exception(() => Assert.Throws(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown() + { + static void testCode() => throw new ArgumentException(); + + Assert.Throws(testCode); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static void testCode() => Assert.Skip("This is a skipped test"); + + try + { + Assert.Throws(testCode); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithActionAndInspector + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.Throws(default(Action)!, _ => null)); + } + + [Fact] + public static void NoExceptionThrown() + { + static void testCode() { } + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorPasses() + { + static void testCode() => throw new ArgumentException(); + + Assert.Throws(testCode, _ => null); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorFails() + { + static void testCode() => throw new ArgumentException(); + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.Throws() Failure: I don't like this exception", ex.Message); Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorThrows() + { + static void testCode() => throw new ArgumentException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception thrown by inspector", + ex.Message + ); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static void testCode() => Assert.Skip("This is a skipped test"); + + try + { + Assert.Throws(testCode, _ => null); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithFunc + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.Throws(default(Func)!)); + } + + [Fact] + public static void ProtectsAgainstAccidentalTask() + { + static object testCode() => Task.FromResult(42); + + var ex = Record.Exception(() => Assert.Throws(testCode)); + + Assert.IsType(ex); + Assert.Equal("You must call Assert.ThrowsAsync when testing async code", ex.Message); + } + + [Fact] + public static void NoExceptionThrown() + { + static object testCode() => 42; + + var ex = Record.Exception(() => Assert.Throws(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown() + { + static object testCode() => throw new ArgumentException(); + + Assert.Throws(testCode); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static object testCode() { Assert.Skip("This is a skipped test"); return null; } + + try + { + Assert.Throws(testCode); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithFuncAndInspector + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.Throws(default(Func)!, _ => null)); + } + + [Fact] + public static void ProtectsAgainstAccidentalTask() + { + static object testCode() => Task.FromResult(42); + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal("You must call Assert.ThrowsAsync when testing async code", ex.Message); + } + + [Fact] + public static void NoExceptionThrown() + { + static object testCode() => 42; + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorPasses() + { + static object testCode() => throw new ArgumentException(); + + Assert.Throws(testCode, _ => null); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorFailed() + { + static object testCode() => throw new ArgumentException(); + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.Throws() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorThrows() + { + static object testCode() => throw new ArgumentException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal("Assert.Throws() Failure: Exception thrown by inspector", ex.Message); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static object testCode() { Assert.Skip("This is a skipped test"); return null; } + + try + { + Assert.Throws(testCode, _ => null); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + } + + public class Throws_Generic_ArgumentException + { + public class WithAction + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.Throws(default(Action)!)); + } + + [Fact] + public static void NoExceptionThrown() + { + static void testCode() { } + + var ex = Record.Exception(() => Assert.Throws("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown() + { + static void testCode() => throw new ArgumentException("Hello world", "paramName"); + + Assert.Throws("paramName", testCode); + } + + [Fact] + public static void IncorrectParameterName() + { + static void testCode() => throw new ArgumentException("Hello world", "paramName1"); + + var ex = Record.Exception(() => Assert.Throws("paramName2", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Incorrect parameter name" + Environment.NewLine + + "Exception: typeof(System.ArgumentException)" + Environment.NewLine + + "Expected: \"paramName2\"" + Environment.NewLine + + "Actual: \"paramName1\"", + ex.Message + ); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + } + + public class WithFunc + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.Throws(default(Func)!)); + } + + [Fact] + public static void NoExceptionThrown() + { + static object testCode() => 42; + + var ex = Record.Exception(() => Assert.Throws("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown() + { + static object testCode() => throw new ArgumentException("Hello world", "paramName"); + + Assert.Throws("paramName", testCode); + } + + [Fact] + public static void IncorrectParameterName() + { + static object testCode() => throw new ArgumentException("Hello world", "paramName1"); + + var ex = Record.Exception(() => Assert.Throws("paramName2", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Incorrect parameter name" + Environment.NewLine + + "Exception: typeof(System.ArgumentException)" + Environment.NewLine + + "Expected: \"paramName2\"" + Environment.NewLine + + "Actual: \"paramName1\"", + ex.Message + ); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.Throws("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + } + } + + public class ThrowsAny + { + public class WithAction + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.ThrowsAny(default(Action)!)); + } + + [Fact] + public static void NoExceptionThrown() + { + static void testCode() { } + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown() + { + static void testCode() => throw new ArgumentException(); + + Assert.ThrowsAny(testCode); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: Exception type was not compatible" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + static void testCode() => throw new ArgumentNullException(); + + Assert.ThrowsAny(testCode); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static void testCode() => Assert.Skip("This is a skipped test"); + + try + { + Assert.ThrowsAny(testCode); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithActionAndInspector + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.ThrowsAny(default(Action)!, _ => null)); + } + + [Fact] + public static void NoExceptionThrown() + { + static void testCode() { } + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorPasses() + { + static void testCode() => throw new ArgumentException(); + + Assert.ThrowsAny(testCode, _ => null); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorFails() + { + static void testCode() => throw new ArgumentException(); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorThrows() + { + static void testCode() => throw new ArgumentException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: Exception thrown by inspector", ex.Message); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + void testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: Exception type was not compatible" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown_InspectorPasses() + { + static void testCode() => throw new ArgumentNullException(); + + Assert.ThrowsAny(testCode, _ => null); + } + + [Fact] + public static void DerivedExceptionThrown_InspectorFails() + { + static void testCode() => throw new ArgumentNullException(); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown_InspectorThrows() + { + static void testCode() => throw new ArgumentNullException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: Exception thrown by inspector", ex.Message); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static void testCode() => Assert.Skip("This is a skipped test"); + + try + { + Assert.ThrowsAny(testCode, _ => null); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithFunc + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.ThrowsAny(default(Func)!)); + } + + [Fact] + public static void ProtectsAgainstAccidentalTask() + { + static object testCode() => Task.FromResult(42); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode)); + + Assert.IsType(ex); + Assert.Equal("You must call Assert.ThrowsAnyAsync when testing async code", ex.Message); + } + + [Fact] + public static void NoExceptionThrown() + { + static object testCode() => 42; + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown() + { + static object testCode() => throw new ArgumentException(); + + Assert.ThrowsAny(testCode); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: Exception type was not compatible" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown() + { + static object testCode() => throw new ArgumentNullException(); + + Assert.ThrowsAny(testCode); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static object testCode() { Assert.Skip("This is a skipped test"); return null; } + + try + { + Assert.ThrowsAny(testCode); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithFuncAndInspector + { + [Fact] + public static void GuardClause() + { + Assert.Throws("testCode", () => Assert.ThrowsAny(default(Func)!, _ => null)); + } + + [Fact] + public static void ProtectsAgainstAccidentalTask() + { + static object testCode() => Task.FromResult(42); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal("You must call Assert.ThrowsAnyAsync when testing async code", ex.Message); + } + + [Fact] + public static void NoExceptionThrown() + { + static object testCode() => 42; + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorPasses() + { + static object testCode() => throw new ArgumentException(); + + Assert.ThrowsAny(testCode, _ => null); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorFails() + { + static object testCode() => throw new ArgumentException(); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void CorrectExceptionThrown_InspectorThrows() + { + static object testCode() => throw new ArgumentException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: Exception thrown by inspector", ex.Message); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static void IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + object testCode() => throw thrown; + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: Exception type was not compatible" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown_InspectorPasses() + { + static object testCode() => throw new ArgumentNullException(); + + Assert.ThrowsAny(testCode, _ => null); + } + + [Fact] + public static void DerivedExceptionThrown_InspectorFails() + { + static object testCode() => throw new ArgumentNullException(); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static void DerivedExceptionThrown_InspectorThrows() + { + static object testCode() => throw new ArgumentNullException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = Record.Exception(() => Assert.ThrowsAny(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: Exception thrown by inspector", ex.Message); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static void SkipExceptionEscapes() + { + static object testCode() { Assert.Skip("This is a skipped test"); return null; } + + try + { + Assert.ThrowsAny(testCode, _ => null); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + } + + public class ThrowsAnyAsync + { + public class WithoutInspector + { + [Fact] + public static async Task GuardClause() + { + await Assert.ThrowsAsync("testCode", () => Assert.ThrowsAnyAsync(default!)); + } + + [Fact] + public static async Task NoExceptionThrown() + { + static Task testCode() => Task.FromResult(42); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAnyAsync(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static async Task CorrectExceptionThrown() + { + static Task testCode() => throw new ArgumentException(); + + await Assert.ThrowsAnyAsync(testCode); + } + + [Fact] + public static async Task IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + Task testCode() => throw thrown; + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAnyAsync(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: Exception type was not compatible" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static async Task DerivedExceptionThrown() + { + static Task testCode() => throw new ArgumentNullException(); + + await Assert.ThrowsAnyAsync(testCode); + } + + [Fact] + public static async Task SkipExceptionEscapes() + { + static Task testCode() { Assert.Skip("This is a skipped test"); return Task.CompletedTask; } + + try + { + await Assert.ThrowsAnyAsync(testCode); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithInspector + { + [Fact] + public static async Task GuardClause() + { + await Assert.ThrowsAsync("testCode", () => Assert.ThrowsAnyAsync(default!, _ => null)); + } + + [Fact] + public static async Task NoExceptionThrown() + { + static Task testCode() => Task.FromResult(42); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAnyAsync(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static async Task CorrectExceptionThrown_InspectorPasses() + { + static Task testCode() => throw new ArgumentException(); + + await Assert.ThrowsAnyAsync(testCode, _ => null); + } + + [Fact] + public static async Task CorrectExceptionThrown_InspectorFails() + { + static Task testCode() => throw new ArgumentException(); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAnyAsync(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static async Task CorrectExceptionThrown_InspectorThrows() + { + static Task testCode() => throw new ArgumentException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAnyAsync(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: Exception thrown by inspector", ex.Message); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static async Task IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + Task testCode() => throw thrown; + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAnyAsync(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ThrowsAny() Failure: Exception type was not compatible" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static async Task DerivedExceptionThrown_InspectorPasses() + { + static Task testCode() => throw new ArgumentNullException(); + + await Assert.ThrowsAnyAsync(testCode, _ => null); + } + + [Fact] + public static async Task DerivedExceptionThrown_InspectorFails() + { + static Task testCode() => throw new ArgumentNullException(); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAnyAsync(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static async Task DerivedExceptionThrown_InspectorThrows() + { + static Task testCode() => throw new ArgumentNullException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAnyAsync(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal("Assert.ThrowsAny() Failure: Exception thrown by inspector", ex.Message); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static async Task SkipExceptionEscapes() + { + static Task testCode() { Assert.Skip("This is a skipped test"); return Task.CompletedTask; } + + try + { + await Assert.ThrowsAnyAsync(testCode, _ => null); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + } + + public class ThrowsAsync + { + public class WithoutInspector + { + [Fact] + public static async Task GuardClause() + { + await Assert.ThrowsAsync("testCode", () => Assert.ThrowsAsync(default!)); + } + + [Fact] + public static async Task NoExceptionThrown() + { + static Task testCode() => Task.FromResult(42); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static async Task CorrectExceptionThrown() + { + static Task testCode() => throw new ArgumentException(); + + await Assert.ThrowsAsync(testCode); + } + + [Fact] + public static async Task IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + Task testCode() => throw thrown; + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static async Task DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + Task testCode() => throw thrown; + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync(testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static async Task SkipExceptionEscapes() + { + static Task testCode() { Assert.Skip("This is a skipped test"); return Task.CompletedTask; } + + try + { + await Assert.ThrowsAsync(testCode); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + + public class WithInspector + { + [Fact] + public static async Task GuardClause() + { + await Assert.ThrowsAsync("testCode", () => Assert.ThrowsAsync(default!, _ => null)); + } + + [Fact] + public static async Task NoExceptionThrown() + { + static Task testCode() => Task.FromResult(42); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static async Task CorrectExceptionThrown_InspectorPasses() + { + static Task testCode() => throw new ArgumentException(); + + await Assert.ThrowsAsync(testCode, _ => null); + } + + [Fact] + public static async Task CorrectExceptionThrown_InspectorFails() + { + static Task testCode() => throw new ArgumentException(); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync(testCode, _ => "I don't like this exception")); + + Assert.IsType(ex); + Assert.Equal("Assert.Throws() Failure: I don't like this exception", ex.Message); + Assert.Null(ex.InnerException); + } + + [Fact] + public static async Task CorrectExceptionThrown_InspectorThrows() + { + static Task testCode() => throw new ArgumentException(); + var thrownByInspector = new DivideByZeroException(); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync(testCode, _ => throw thrownByInspector)); + + Assert.IsType(ex); + Assert.Equal("Assert.Throws() Failure: Exception thrown by inspector", ex.Message); + Assert.Same(thrownByInspector, ex.InnerException); + } + + [Fact] + public static async Task IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + Task testCode() => throw thrown; + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static async Task DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + Task testCode() => throw thrown; + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync(testCode, _ => null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static async Task SkipExceptionEscapes() + { + static Task testCode() { Assert.Skip("This is a skipped test"); return Task.CompletedTask; } + + try + { + await Assert.ThrowsAsync(testCode, _ => null); + Assert.Fail("The exception should not be caught"); + } + catch (Exception ex) + { + Assert.Equal(DynamicSkipToken.Value + "This is a skipped test", ex.Message); + } + } + } + } + + public class ThrowsAsync_ArgumentException + { + [Fact] + public static async Task GuardClause() + { + await Assert.ThrowsAsync("testCode", () => Assert.ThrowsAsync("paramName", default!)); + } + + [Fact] + public static async Task NoExceptionThrown() + { + static Task testCode() => Task.FromResult(42); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)", + ex.Message + ); + Assert.Null(ex.InnerException); + } + + [Fact] + public static async Task CorrectExceptionThrown() + { + static Task testCode() => throw new ArgumentException("Hello world", "paramName"); + + await Assert.ThrowsAsync("paramName", testCode); + } + + [Fact] + public static async Task IncorrectParameterName() + { + static Task testCode() => throw new ArgumentException("Hello world", "paramName1"); + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync("paramName2", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Incorrect parameter name" + Environment.NewLine + + "Exception: typeof(System.ArgumentException)" + Environment.NewLine + + "Expected: \"paramName2\"" + Environment.NewLine + + "Actual: \"paramName1\"", + ex.Message + ); + } + + [Fact] + public static async Task IncorrectExceptionThrown() + { + var thrown = new DivideByZeroException(); + Task testCode() => throw thrown; + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.DivideByZeroException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + + [Fact] + public static async Task DerivedExceptionThrown() + { + var thrown = new ArgumentNullException(); + Task testCode() => throw thrown; + + var ex = await Record.ExceptionAsync(() => Assert.ThrowsAsync("paramName", testCode)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + + "Expected: typeof(System.ArgumentException)" + Environment.NewLine + + "Actual: typeof(System.ArgumentNullException)", + ex.Message + ); + Assert.Same(thrown, ex.InnerException); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/FailAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/FailAssertsTests.cs new file mode 100644 index 00000000000..b8126445c01 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/FailAssertsTests.cs @@ -0,0 +1,23 @@ +using Xunit; +using Xunit.Sdk; + +public class FailAssertsTests +{ + [Fact] + public void WithoutMessage() + { + var ex = Record.Exception(() => Assert.Fail()); + + Assert.IsType(ex); + Assert.Equal("Assert.Fail() Failure", ex.Message); + } + + [Fact] + public void WithMessage() + { + var ex = Record.Exception(() => Assert.Fail("This is a user message")); + + Assert.IsType(ex); + Assert.Equal("This is a user message", ex.Message); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/IdentityAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/IdentityAssertsTests.cs new file mode 100644 index 00000000000..7b19c4731f1 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/IdentityAssertsTests.cs @@ -0,0 +1,58 @@ +using Xunit; +using Xunit.Sdk; + +public class IdentityAssertsTests +{ + public class NotSame + { + [Fact] + public void Identical() + { + var actual = new object(); + + var ex = Record.Exception(() => Assert.NotSame(actual, actual)); + + Assert.IsType(ex); + Assert.Equal("Assert.NotSame() Failure: Values are the same instance", ex.Message); + } + + [Fact] + public void NotIdentical() + { + Assert.NotSame("bob", "jim"); + } + } + + public class Same + { + [Fact] + public void Identical() + { + var actual = new object(); + + Assert.Same(actual, actual); + } + + [Fact] + public void NotIdentical() + { + var ex = Record.Exception(() => Assert.Same("bob", "jim")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Same() Failure: Values are not the same instance" + Environment.NewLine + + "Expected: \"bob\"" + Environment.NewLine + + "Actual: \"jim\"", + ex.Message + ); + } + + [Fact] + public void EqualValueTypeValuesAreNotSameBecauseOfBoxing() + { +#pragma warning disable xUnit2005 // Do not use identity check on value type + Assert.Throws(() => Assert.Same(0, 0)); +#pragma warning restore xUnit2005 // Do not use identity check on value type + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/MemoryAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/MemoryAssertsTests.cs new file mode 100644 index 00000000000..58abb6dcfbe --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/MemoryAssertsTests.cs @@ -0,0 +1,940 @@ +using Xunit; +using Xunit.Sdk; + +public class MemoryAssertsTests +{ + public class Contains + { + public class Strings + { + [Fact] + public void ReadOnlyMemory_Success() + { + Assert.Contains("wor".AsMemory(), "Hello, world!".AsMemory()); + } + + [Fact] + public void ReadWriteMemory_Success() + { + Assert.Contains("wor".Memoryify(), "Hello, world!".Memoryify()); + } + + [Fact] + public void ReadOnlyMemory_CaseSensitiveByDefault() + { + var ex = Record.Exception(() => Assert.Contains("WORLD".AsMemory(), "Hello, world!".AsMemory())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Not found: \"WORLD\"", + ex.Message + ); + } + + [Fact] + public void ReadWriteMemory_CaseSensitiveByDefault() + { + var ex = Record.Exception(() => Assert.Contains("WORLD".Memoryify(), "Hello, world!".Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Not found: \"WORLD\"", + ex.Message + ); + } + + [Fact] + public void ReadOnlyMemory_CanSpecifyComparisonType() + { + Assert.Contains("WORLD".AsMemory(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ReadWriteMemory_CanSpecifyComparisonType() + { + Assert.Contains("WORLD".Memoryify(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ReadOnlyMemory_NullStringIsEmpty() + { + var ex = Record.Exception(() => Assert.Contains("foo".AsMemory(), default(string).AsMemory())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"\"" + Environment.NewLine + + "Not found: \"foo\"", + ex.Message + ); + } + + [Fact] + public void ReadWriteMemory_NullStringIsEmpty() + { + var ex = Record.Exception(() => Assert.Contains("foo".Memoryify(), default(string).Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"\"" + Environment.NewLine + + "Not found: \"foo\"", + ex.Message + ); + } + + [Fact] + public void VeryLongStrings() + { + var ex = Record.Exception( + () => Assert.Contains( + "We are looking for something that is actually very long as well".Memoryify(), + "This is a relatively long string so that we can see the truncation in action".Memoryify() + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"This is a relatively long string so that we can se\"" + ArgumentFormatter.Ellipsis + Environment.NewLine + + "Not found: \"We are looking for something that is actually very\"" + ArgumentFormatter.Ellipsis, + ex.Message + ); + } + } + + public class NonStrings + { + [Fact] + public void ReadOnlyMemoryOfInts_Success() + { + Assert.Contains(new int[] { 3, 4 }.AsMemory(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.AsMemory()); + } + + [Fact] + public void ReadOnlyMemoryOfStrings_Success() + { + Assert.Contains(new string[] { "test", "it" }.AsMemory(), new string[] { "something", "interesting", "test", "it", "out" }.AsMemory()); + } + + [Fact] + public void ReadWriteMemoryOfInts_Success() + { + Assert.Contains(new int[] { 3, 4 }.Memoryify(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.Memoryify()); + } + + [Fact] + public void ReadWriteMemoryOfStrings_Success() + { + Assert.Contains(new string[] { "test", "it" }.Memoryify(), new string[] { "something", "interesting", "test", "it", "out" }.Memoryify()); + } + + [Fact] + public void ReadOnlyMemoryOfInts_Failure() + { + var ex = Record.Exception(() => Assert.Contains(new int[] { 13, 14 }.AsMemory(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.AsMemory())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-memory not found" + Environment.NewLine + + "Memory: [1, 2, 3, 4, 5, " + ArgumentFormatter.Ellipsis + "]" + Environment.NewLine + + "Not found: [13, 14]", + ex.Message + ); + } + + [Fact] + public void ReadWriteMemoryOfInts_Failure() + { + var ex = Record.Exception(() => Assert.Contains(new int[] { 13, 14 }.Memoryify(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-memory not found" + Environment.NewLine + + "Memory: [1, 2, 3, 4, 5, " + ArgumentFormatter.Ellipsis + "]" + Environment.NewLine + + "Not found: [13, 14]", + ex.Message + ); + } + + [Fact] + public void FindingNonEmptyMemoryInsideEmptyMemoryFails() + { + var ex = Record.Exception(() => Assert.Contains(new int[] { 3, 4 }.Memoryify(), Memory.Empty)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-memory not found" + Environment.NewLine + + "Memory: []" + Environment.NewLine + + "Not found: [3, 4]", + ex.Message + ); + } + + [Fact] + public void FindingEmptyMemoryInsideAnyMemorySucceeds() + { + Assert.Contains(Memory.Empty, new int[] { 3, 4 }.Memoryify()); + Assert.Contains(Memory.Empty, Memory.Empty); + } + } + } + + public class DoesNotContain + { + public class Strings + { + [Fact] + public void ReadOnlyMemory_Success() + { + Assert.DoesNotContain("hey".AsMemory(), "Hello, world!".AsMemory()); + } + + [Fact] + public void ReadWriteMemory_Success() + { + Assert.DoesNotContain("hey".Memoryify(), "Hello, world!".Memoryify()); + } + + [Fact] + public void ReadOnlyMemory_CaseSensitiveByDefault() + { + Assert.DoesNotContain("WORLD".AsMemory(), "Hello, world!".AsMemory()); + } + + [Fact] + public void ReadWriteMemory_CaseSensitiveByDefault() + { + Assert.DoesNotContain("WORLD".Memoryify(), "Hello, world!".Memoryify()); + } + + [Fact] + public void ReadOnlyMemory_CanSpecifyComparisonType() + { + var ex = Record.Exception(() => Assert.DoesNotContain("WORLD".AsMemory(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"WORLD\"", + ex.Message + ); + } + + [Fact] + public void ReadWriteMemory_CanSpecifyComparisonType() + { + var ex = Record.Exception(() => Assert.DoesNotContain("WORLD".Memoryify(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"WORLD\"", + ex.Message + ); + } + + [Fact] + public void ReadOnlyMemory_Failure() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".AsMemory(), "Hello, world!".AsMemory())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + [Fact] + public void ReadWriteMemory_Failure() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".Memoryify(), "Hello, world!".Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + [Fact] + public void ReadOnlyMemory_NullStringIsEmpty() + { + Assert.DoesNotContain("foo".AsMemory(), default(string).AsMemory()); + } + + [Fact] + public void ReadWriteMemory_NullStringIsEmpty() + { + Assert.DoesNotContain("foo".Memoryify(), default(string).Memoryify()); + } + + [Fact] + public void VeryLongString_FoundAtFront() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".Memoryify(), "Hello, world from a very long string that will end up being truncated".Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world from a very long string that will end\"" + ArgumentFormatter.Ellipsis + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + [Fact] + public void VeryLongString_FoundInMiddle() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".Memoryify(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 50)" + Environment.NewLine + + "String: " + ArgumentFormatter.Ellipsis + "\" string that has 'Hello, world' placed in the midd\"" + ArgumentFormatter.Ellipsis + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + [Fact] + public void VeryLongString_FoundAtEnd() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".Memoryify(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 89)" + Environment.NewLine + + "String: " + ArgumentFormatter.Ellipsis + "\"om the front truncated, just to say 'Hello, world'\"" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + } + + public class NonStrings + { + [Fact] + public void ReadOnlyMemoryOfInts_Success() + { + Assert.DoesNotContain(new int[] { 13, 14 }.AsMemory(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.AsMemory()); + } + + [Fact] + public void ReadOnlyMemoryOfStrings_Success() + { + Assert.DoesNotContain(new string[] { "it", "test" }.AsMemory(), new string[] { "something", "interesting", "test", "it", "out" }.AsMemory()); + } + + [Fact] + public void ReadWriteMemoryOfInts_Success() + { + Assert.DoesNotContain(new int[] { 13, 14 }.Memoryify(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.Memoryify()); + } + + [Fact] + public void ReadWriteMemoryOfStrings_Success() + { + Assert.DoesNotContain(new string[] { "it", "test" }.Memoryify(), new string[] { "something", "interesting", "test", "it", "out" }.Memoryify()); + } + + [Fact] + public void ReadOnlyMemoryOfInts_Failure() + { + var ex = Record.Exception(() => Assert.DoesNotContain(new int[] { 3, 4 }.AsMemory(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.AsMemory())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-memory found" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + "Memory: [1, 2, 3, 4, 5, " + ArgumentFormatter.Ellipsis + "]" + Environment.NewLine + + "Found: [3, 4]", + ex.Message + ); + } + + [Fact] + public void ReadWriteMemoryOfInts_Failure() + { + var ex = Record.Exception(() => Assert.DoesNotContain(new int[] { 3, 4 }.Memoryify(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-memory found" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + "Memory: [1, 2, 3, 4, 5, " + ArgumentFormatter.Ellipsis + "]" + Environment.NewLine + + "Found: [3, 4]", + ex.Message + ); + } + + [Fact] + public void SearchingForNonEmptyMemoryInsideEmptyMemorySucceeds() + { + Assert.DoesNotContain(new int[] { 3, 4 }.Memoryify(), Memory.Empty); + } + + [Theory] + [InlineData(new[] { 3, 4 })] + [InlineData(new int[0])] + public void SearchForEmptyMemoryInsideAnyMemoryFails(IEnumerable data) + { + var ex = Record.Exception(() => Assert.DoesNotContain(Memory.Empty, data.ToArray().Memoryify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-memory found" + Environment.NewLine + + (data.Any() ? " ↓ (pos 0)" + Environment.NewLine : "") + + "Memory: " + CollectionTracker.FormatStart(data) + Environment.NewLine + + "Found: []", + ex.Message + ); + } + } + } + + public class EndsWith + { + [Fact] + public void Success() + { + Assert.EndsWith("world!".AsMemory(), "Hello, world!".AsMemory()); + Assert.EndsWith("world!".AsMemory(), "Hello, world!".Memoryify()); + Assert.EndsWith("world!".Memoryify(), "Hello, world!".AsMemory()); + Assert.EndsWith("world!".Memoryify(), "Hello, world!".Memoryify()); + } + + [Fact] + public void Failure() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Expected end: \"hey\"", + ex.Message + ); + } + + assertFailure(() => Assert.EndsWith("hey".AsMemory(), "Hello, world!".AsMemory())); + assertFailure(() => Assert.EndsWith("hey".AsMemory(), "Hello, world!".Memoryify())); + assertFailure(() => Assert.EndsWith("hey".Memoryify(), "Hello, world!".AsMemory())); + assertFailure(() => Assert.EndsWith("hey".Memoryify(), "Hello, world!".Memoryify())); + } + + [Fact] + public void CaseSensitiveByDefault() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: \"world!\"" + Environment.NewLine + + "Expected end: \"WORLD!\"", + ex.Message + ); + } + + assertFailure(() => Assert.EndsWith("WORLD!".AsMemory(), "world!".AsMemory())); + assertFailure(() => Assert.EndsWith("WORLD!".AsMemory(), "world!".Memoryify())); + assertFailure(() => Assert.EndsWith("WORLD!".Memoryify(), "world!".AsMemory())); + assertFailure(() => Assert.EndsWith("WORLD!".Memoryify(), "world!".Memoryify())); + } + + [Fact] + public void CanSpecifyComparisonType() + { + Assert.EndsWith("WORLD!".AsMemory(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".AsMemory(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".Memoryify(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".Memoryify(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NullStringIsEmpty() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: \"\"" + Environment.NewLine + + "Expected end: \"foo\"", + ex.Message + ); + } + + assertFailure(() => Assert.EndsWith("foo".AsMemory(), null)); + assertFailure(() => Assert.EndsWith("foo".Memoryify(), null)); + } + + [Fact] + public void Truncation() + { + var expected = "This is a long string that we're looking for at the end"; + var actual = "This is the long string that we expected to find this ending inside"; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: " + ArgumentFormatter.Ellipsis + "\"string that we expected to find this ending inside\"" + Environment.NewLine + + "Expected end: \"This is a long string that we're looking for at th\"" + ArgumentFormatter.Ellipsis, + ex.Message + ); + } + + assertFailure(() => Assert.EndsWith(expected.AsMemory(), actual.AsMemory())); + assertFailure(() => Assert.EndsWith(expected.AsMemory(), actual.Memoryify())); + assertFailure(() => Assert.EndsWith(expected.Memoryify(), actual.AsMemory())); + assertFailure(() => Assert.EndsWith(expected.Memoryify(), actual.Memoryify())); + } + } + + public class Equal + { + public class Chars_TreatedLikeStrings + { + [Theory] + // Null values + [InlineData(null, null, false, false, false, false)] + // Null ReadOnlySpan acts like an empty string + [InlineData(null, "", false, false, false, false)] + [InlineData("", null, false, false, false, false)] + // Empty values + [InlineData("", "", false, false, false, false)] + // Identical values + [InlineData("foo", "foo", false, false, false, false)] + // Case differences + [InlineData("foo", "FoO", true, false, false, false)] + // Line ending differences + [InlineData("foo \r\n bar", "foo \r bar", false, true, false, false)] + [InlineData("foo \r\n bar", "foo \n bar", false, true, false, false)] + [InlineData("foo \n bar", "foo \r bar", false, true, false, false)] + // Whitespace differences + [InlineData(" ", "\t", false, false, true, false)] + [InlineData(" \t", "\t ", false, false, true, false)] + [InlineData(" ", "\t", false, false, true, false)] + [InlineData(" ", " \u180E", false, false, true, false)] + [InlineData(" \u180E", "\u180E ", false, false, true, false)] + [InlineData(" ", "\u180E", false, false, true, false)] + [InlineData(" ", " \u200B", false, false, true, false)] + [InlineData(" \u200B", "\u200B ", false, false, true, false)] + [InlineData(" ", "\u200B", false, false, true, false)] + [InlineData(" ", " \u200B\uFEFF", false, false, true, false)] + [InlineData(" \u180E", "\u200B\u202F\u1680\u180E ", false, false, true, false)] + [InlineData("\u2001\u2002\u2003\u2006\u2009 ", "\u200B", false, false, true, false)] + [InlineData("\u00A0\u200A\u2009\u2006\u2009 ", "\u200B", false, false, true, false)] + // The ogham space mark (\u1680) kind of looks like a faint dash, but Microsoft has put it + // inside the SpaceSeparator unicode category, so we also treat it as a space + [InlineData("\u2007\u2008\u1680\t\u0009\u3000 ", " ", false, false, true, false)] + [InlineData("\u1680", "\t", false, false, true, false)] + [InlineData("\u1680", " ", false, false, true, false)] + // All whitespace differences + [InlineData("", " ", false, false, false, true)] + [InlineData("", " ", false, false, true, true)] + [InlineData("", "\t", false, false, true, true)] + [InlineData("foobar", "foo bar", false, false, true, true)] + public void Success( + string? value1, + string? value2, + bool ignoreCase, + bool ignoreLineEndingDifferences, + bool ignoreWhiteSpaceDifferences, + bool ignoreAllWhiteSpace) + { + // Run everything in both directions, as the values should be interchangeable when they're equal + + // ReadOnlyMemory vs. ReadOnlyMemory + Assert.Equal(value1.AsMemory(), value2.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.AsMemory(), value1.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + // ReadOnlyMemory vs. Memory + Assert.Equal(value1.AsMemory(), value2.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.AsMemory(), value1.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + // Memory vs. ReadOnlyMemory + Assert.Equal(value1.Memoryify(), value2.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.Memoryify(), value1.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + // Memory vs. Memory + Assert.Equal(value1.Memoryify(), value2.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.Memoryify(), value1.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + } + + [Theory] + // Non-identical values + [InlineData("foo", "foo!", false, false, false, false, null, " ↑ (pos 3)")] + [InlineData("foo\0", "foo\0\0", false, false, false, false, null, " ↑ (pos 4)")] + // Overruns + [InlineData("first test", "first test 1", false, false, false, false, null, " ↑ (pos 10)")] + [InlineData("first test 1", "first test", false, false, false, false, " ↓ (pos 10)", null)] + // Case differences + [InlineData("Foobar", "foo bar", true, false, false, false, " ↓ (pos 3)", " ↑ (pos 3)")] + // Line ending differences + [InlineData("foo\nbar", "foo\rBar", false, true, false, false, " ↓ (pos 4)", " ↑ (pos 4)")] + // Non-zero whitespace quantity differences + [InlineData("foo bar", "foo Bar", false, false, true, false, " ↓ (pos 4)", " ↑ (pos 5)")] + // Ignore all white space differences + [InlineData("foobar", "foo Bar", false, false, false, true, " ↓ (pos 3)", " ↑ (pos 4)")] + public void Failure( + string expected, + string actual, + bool ignoreCase, + bool ignoreLineEndingDifferences, + bool ignoreWhiteSpaceDifferences, + bool ignoreAllWhiteSpace, + string? expectedPointer, + string? actualPointer) + { + var message = "Assert.Equal() Failure: Strings differ"; + + if (expectedPointer is not null) + message += Environment.NewLine + " " + expectedPointer; + + message += + Environment.NewLine + "Expected: " + ArgumentFormatter.Format(expected) + + Environment.NewLine + "Actual: " + ArgumentFormatter.Format(actual); + + if (actualPointer is not null) + message += Environment.NewLine + " " + actualPointer; + + void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + message, + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected.AsMemory(), actual.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + assertFailure(() => Assert.Equal(expected.Memoryify(), actual.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + assertFailure(() => Assert.Equal(expected.AsMemory(), actual.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + assertFailure(() => Assert.Equal(expected.Memoryify(), actual.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + } + + [Fact] + public void Truncation() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 21)" + Environment.NewLine + + "Expected: \"Why hello there world, you're a long string with s\"" + ArgumentFormatter.Ellipsis + Environment.NewLine + + "Actual: \"Why hello there world! You're a long string!\"" + Environment.NewLine + + " ↑ (pos 21)", + ex.Message + ); + } + + assertFailure( + () => Assert.Equal( + "Why hello there world, you're a long string with some truncation!".AsMemory(), + "Why hello there world! You're a long string!".AsMemory() + ) + ); + assertFailure( + () => Assert.Equal( + "Why hello there world, you're a long string with some truncation!".AsMemory(), + "Why hello there world! You're a long string!".Memoryify() + ) + ); + assertFailure( + () => Assert.Equal( + "Why hello there world, you're a long string with some truncation!".Memoryify(), + "Why hello there world! You're a long string!".AsMemory() + ) + ); + assertFailure( + () => Assert.Equal( + "Why hello there world, you're a long string with some truncation!".Memoryify(), + "Why hello there world! You're a long string!".Memoryify() + ) + ); + } + } + + public class Ints + { + [Theory] + // Null values + [InlineData(null, null)] + [InlineData(null, new int[] { })] // Null ReadOnlySpan acts like an empty array + [InlineData(new int[] { }, null)] + // Identical values + [InlineData(new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 })] + public void Success( + int[]? value1, + int[]? value2) + { + // Run them in both directions, as the values should be interchangeable when they're equal + + // ReadOnlySpan vs. ReadOnlySpan + Assert.Equal(value1.AsMemory(), value2.AsMemory()); + Assert.Equal(value2.AsMemory(), value1.AsMemory()); + + // ReadOnlySpan vs. Span + Assert.Equal(value1.AsMemory(), value2.Memoryify()); + Assert.Equal(value2.AsMemory(), value1.Memoryify()); + + // Span vs. ReadOnlySpan + Assert.Equal(value1.Memoryify(), value2.AsMemory()); + Assert.Equal(value2.Memoryify(), value1.AsMemory()); + + // Span vs. Span + Assert.Equal(value1.Memoryify(), value2.Memoryify()); + Assert.Equal(value2.Memoryify(), value1.Memoryify()); + } + + [Fact] + public void Failure_MidCollection() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Expected: [1, 0, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3]" + Environment.NewLine + + " ↑ (pos 1)", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(new int[] { 1, 0, 2, 3 }.AsMemory(), new int[] { 1, 2, 3 }.AsMemory())); + assertFailure(() => Assert.Equal(new int[] { 1, 0, 2, 3 }.AsMemory(), new int[] { 1, 2, 3 }.Memoryify())); + assertFailure(() => Assert.Equal(new int[] { 1, 0, 2, 3 }.Memoryify(), new int[] { 1, 2, 3 }.AsMemory())); + assertFailure(() => Assert.Equal(new int[] { 1, 0, 2, 3 }.Memoryify(), new int[] { 1, 2, 3 }.Memoryify())); + } + + [Fact] + public void Failure_BeyondEnd() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3, 4]" + Environment.NewLine + + " ↑ (pos 3)", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(new int[] { 1, 2, 3 }.AsMemory(), new int[] { 1, 2, 3, 4 }.AsMemory())); + assertFailure(() => Assert.Equal(new int[] { 1, 2, 3 }.AsMemory(), new int[] { 1, 2, 3, 4 }.Memoryify())); + assertFailure(() => Assert.Equal(new int[] { 1, 2, 3 }.Memoryify(), new int[] { 1, 2, 3, 4 }.AsMemory())); + assertFailure(() => Assert.Equal(new int[] { 1, 2, 3 }.Memoryify(), new int[] { 1, 2, 3, 4 }.Memoryify())); + } + } + + public class Strings + { + [Theory] + // Null values + [InlineData(null, null)] + [InlineData(null, new string[] { })] // Null ReadOnlyMemory acts like an empty array + [InlineData(new string[] { }, null)] + // Identical values + [InlineData(new string[] { "yes", "no", "maybe" }, new string[] { "yes", "no", "maybe" })] + public void Success( + string[]? value1, + string[]? value2) + { + // Run them in both directions, as the values should be interchangeable when they're equal + + // ReadOnlyMemory vs. ReadOnlyMemory + Assert.Equal(value1.AsMemory(), value2.AsMemory()); + Assert.Equal(value2.AsMemory(), value1.AsMemory()); + + // ReadOnlyMemory vs. Memory + Assert.Equal(value1.AsMemory(), value2.Memoryify()); + Assert.Equal(value2.AsMemory(), value1.Memoryify()); + + // Memory vs. ReadOnlyMemory + Assert.Equal(value1.Memoryify(), value2.AsMemory()); + Assert.Equal(value2.Memoryify(), value1.AsMemory()); + + // Memory vs. Memory + Assert.Equal(value1.Memoryify(), value2.Memoryify()); + Assert.Equal(value2.Memoryify(), value1.Memoryify()); + } + + [Fact] + public void Failure() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: [\"yes\", \"no\", \"maybe\"]" + Environment.NewLine + + "Actual: [\"yes\", \"no\", \"maybe\", \"so\"]" + Environment.NewLine + + " ↑ (pos 3)", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(new string[] { "yes", "no", "maybe" }.AsMemory(), new string[] { "yes", "no", "maybe", "so" }.AsMemory())); + assertFailure(() => Assert.Equal(new string[] { "yes", "no", "maybe" }.AsMemory(), new string[] { "yes", "no", "maybe", "so" }.Memoryify())); + assertFailure(() => Assert.Equal(new string[] { "yes", "no", "maybe" }.Memoryify(), new string[] { "yes", "no", "maybe", "so" }.AsMemory())); + assertFailure(() => Assert.Equal(new string[] { "yes", "no", "maybe" }.Memoryify(), new string[] { "yes", "no", "maybe", "so" }.Memoryify())); + } + } + } + + public class StartsWith + { + [Fact] + public void Success() + { + Assert.StartsWith("Hello".AsMemory(), "Hello, world!".AsMemory()); + Assert.StartsWith("Hello".AsMemory(), "Hello, world!".Memoryify()); + Assert.StartsWith("Hello".Memoryify(), "Hello, world!".AsMemory()); + Assert.StartsWith("Hello".Memoryify(), "Hello, world!".Memoryify()); + } + + [Fact] + public void Failure() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Expected start: \"hey\"", + ex.Message + ); + } + + assertFailure(() => Assert.StartsWith("hey".AsMemory(), "Hello, world!".AsMemory())); + assertFailure(() => Assert.StartsWith("hey".AsMemory(), "Hello, world!".Memoryify())); + assertFailure(() => Assert.StartsWith("hey".Memoryify(), "Hello, world!".AsMemory())); + assertFailure(() => Assert.StartsWith("hey".Memoryify(), "Hello, world!".Memoryify())); + } + + [Fact] + public void CaseSensitiveByDefault() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"world!\"" + Environment.NewLine + + "Expected start: \"WORLD!\"", + ex.Message + ); + } + + assertFailure(() => Assert.StartsWith("WORLD!".AsMemory(), "world!".AsMemory())); + assertFailure(() => Assert.StartsWith("WORLD!".AsMemory(), "world!".Memoryify())); + assertFailure(() => Assert.StartsWith("WORLD!".Memoryify(), "world!".AsMemory())); + assertFailure(() => Assert.StartsWith("WORLD!".Memoryify(), "world!".Memoryify())); + } + + [Fact] + public void CanSpecifyComparisonType() + { + Assert.StartsWith("HELLO".AsMemory(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".AsMemory(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".Memoryify(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".Memoryify(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NullStringIsEmpty() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"\"" + Environment.NewLine + + "Expected start: \"foo\"", + ex.Message + ); + } + + assertFailure(() => Assert.StartsWith("foo".AsMemory(), null)); + assertFailure(() => Assert.StartsWith("foo".Memoryify(), null)); + } + + [Fact] + public void Truncation() + { + var expected = "This is a long string that we're looking for at the start"; + var actual = "This is the long string that we expected to find this starting inside"; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"This is the long string that we expected to find t\"" + ArgumentFormatter.Ellipsis + Environment.NewLine + + "Expected start: \"This is a long string that we're looking for at th\"" + ArgumentFormatter.Ellipsis, + ex.Message + ); + } + + assertFailure(() => Assert.StartsWith(expected.AsMemory(), actual.AsMemory())); + assertFailure(() => Assert.StartsWith(expected.AsMemory(), actual.Memoryify())); + assertFailure(() => Assert.StartsWith(expected.Memoryify(), actual.AsMemory())); + assertFailure(() => Assert.StartsWith(expected.Memoryify(), actual.Memoryify())); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/MultipleAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/MultipleAssertsTests.cs new file mode 100644 index 00000000000..7570481066e --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/MultipleAssertsTests.cs @@ -0,0 +1,137 @@ +using Xunit; +using Xunit.Sdk; + +public class MultipleAssertsTests +{ + [Fact] + public void NoActions_DoesNotThrow() + { + Assert.Multiple(); + } + + [Fact] + public void SingleAssert_Success_DoesNotThrow() + { + Assert.Multiple( + static () => Assert.True(true) + ); + } + + [Fact] + public void SingleAssert_Fails_ThrowsFailingAssert() + { + var ex = Record.Exception(() => + Assert.Multiple( + static () => Assert.True(false) + ) + ); + + Assert.IsType(ex); + } + + [Fact] + public void MultipleAssert_Success_DoesNotThrow() + { + Assert.Multiple( + static () => Assert.True(true), + static () => Assert.False(false) + ); + } + + [Fact] + public void MultipleAssert_SingleFailure_ThrowsFailingAssert() + { + var ex = Record.Exception(static () => + Assert.Multiple( + () => Assert.True(true), + () => Assert.False(true) + ) + ); + + Assert.IsType(ex); + } + + [Fact] + public void MultipleAssert_MultipleFailures_ThrowsMultipleException() + { + var ex = Record.Exception(static () => + Assert.Multiple( + () => Assert.True(false), + () => Assert.False(true) + ) + ); + + var multiEx = Assert.IsType(ex); + Assert.Equal( + "Assert.Multiple() Failure: Multiple failures were encountered", + ex.Message + ); + Assert.Collection( + multiEx.InnerExceptions, + innerEx => Assert.IsType(innerEx), + innerEx => Assert.IsType(innerEx) + ); + } + + [Fact] + public async Task MultipleAsync_NoActions_DoesNotThrow() + { + await Assert.MultipleAsync(); + } + + [Fact] + public async Task MultipleAsync_SingleAssert_Success_DoesNotThrow() + { + static Task task(bool isTrue) => Task.FromResult(isTrue); + + await Assert.MultipleAsync( + static async () => Assert.True(await task(true)) + ); + } + + [Fact] + public async Task MultipleAsync_Success_DoesNotThrow() + { + static Task task(bool isTrue) => Task.FromResult(isTrue); + + await Assert.MultipleAsync( + static async () => Assert.True(await task(true)), + static async () => Assert.True(await task(true)), + static async () => Assert.True(await task(true)) + ); + } + + [Fact] + public async Task MultipleAsync_SingleAssert_Fails_ThrowsFailingAssert() + { + static Task task(bool isTrue) => Task.FromResult(isTrue); + + var ex = await Record.ExceptionAsync(static async () => + await Assert.MultipleAsync( + static async () => Assert.False(await task(true)) + ) + ); + + Assert.IsType(ex); + } + + [Fact] + public async Task MultipleAsync_SingleAssert_Multiple_ThrowsFailingAssert() + { + static Task task(bool isTrue) => Task.FromResult(isTrue); + + var ex = await Record.ExceptionAsync(static async () => + await Assert.MultipleAsync( + static async () => Assert.False(await task(true)), + static async () => Assert.False(await task(true)) + ) + ); + + var multiEx = Assert.IsType(ex); + Assert.Collection( + multiEx.InnerExceptions, + static innerEx => Assert.IsType(innerEx), + static innerEx => Assert.IsType(innerEx) + ); + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/NullAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/NullAssertsTests.cs new file mode 100644 index 00000000000..4f3d60b59b3 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/NullAssertsTests.cs @@ -0,0 +1,146 @@ +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + +using Xunit; +using Xunit.Sdk; + +public class NullAssertsTests +{ + public class NotNull + { + [Fact] + public void Success_Reference() + { + Assert.NotNull(new object()); + } + + [Fact] + public void Success_NullableStruct() + { + int? x = 42; + + var result = Assert.NotNull(x); + + Assert.IsType(result); + Assert.Equal(42, result); + } + + +#if XUNIT_POINTERS + [Theory] + [InlineData(42)] + [InlineData("Hello, world")] + public unsafe void Success_Pointer(T data) + { + Assert.NotNull(&data); + } +#endif + + [Fact] + public void Failure_Reference() + { + var ex = Record.Exception(() => Assert.NotNull(null)); + + Assert.IsType(ex); + Assert.Equal("Assert.NotNull() Failure: Value is null", ex.Message); + } + + [Fact] + public void Failure_NullableStruct() + { + int? value = null; + + var ex = Record.Exception(() => Assert.NotNull(value)); + + Assert.IsType(ex); + Assert.Equal("Assert.NotNull() Failure: Value of type 'Nullable' does not have a value", ex.Message); + } + + +#if XUNIT_POINTERS + [Fact] + public unsafe void Failure_Pointer() + { + var ex = Record.Exception(() => Assert.NotNull((object*)null)); + + Assert.IsType(ex); + Assert.Equal("Assert.NotNull() Failure: Value of type 'object*' is null", ex.Message); + } +#endif + } + + public class Null + { + [Fact] + public void Success_Reference() + { + Assert.Null(null); + } + + [Fact] + public void Success_NullableStruct() + { + int? x = null; + + Assert.Null(x); + } + + +#if XUNIT_POINTERS + [Fact] + public unsafe void Success_Pointer() + { + Assert.Null((object*)null); + } +#endif + + [Fact] + public void Failure_Reference() + { + var ex = Record.Exception(() => Assert.Null(new object())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Null() Failure: Value is not null" + Environment.NewLine + + "Expected: null" + Environment.NewLine + +#if XUNIT_AOT + $"Actual: Object {{ {ArgumentFormatter.Ellipsis} }}", +#else + "Actual: Object { }", +#endif + ex.Message + ); + } + + [Fact] + public void Failure_NullableStruct() + { + int? x = 42; + + var ex = Record.Exception(() => Assert.Null(x)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Null() Failure: Value of type 'Nullable' has a value" + Environment.NewLine + + "Expected: null" + Environment.NewLine + + "Actual: 42", + ex.Message + ); + } + + +#if XUNIT_POINTERS + [Theory] + [InlineData(42)] + [InlineData("Hello, world")] + public unsafe void Failure_Pointer(T data) + { + var ptr = &data; + + var ex = Record.Exception(() => Assert.Null(ptr)); + + Assert.IsType(ex); + Assert.Equal($"Assert.Null() Failure: Value of type '{ArgumentFormatter.FormatTypeName(typeof(T))}*' is not null", ex.Message); + } +#endif + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/PropertyAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/PropertyAssertsTests.cs new file mode 100644 index 00000000000..e8504f148d7 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/PropertyAssertsTests.cs @@ -0,0 +1,133 @@ +using System.ComponentModel; +using Xunit; +using Xunit.Sdk; + +public class PropertyAssertsTests +{ + public class PropertyChanged + { + [Fact] + public void GuardClauses() + { + Assert.Throws("object", () => Assert.PropertyChanged(null!, "propertyName", delegate { })); + Assert.Throws("testCode", () => Assert.PropertyChanged(new NotifiedClass(), "propertyName", (Action)null!)); + } + + [Fact] + public void ExceptionThrownWhenPropertyNotChanged() + { + var obj = new NotifiedClass(); + + var ex = Record.Exception(() => Assert.PropertyChanged(obj, nameof(NotifiedClass.Property1), () => { })); + + Assert.IsType(ex); + Assert.Equal("Assert.PropertyChanged() failure: Property 'Property1' was not set", ex.Message); + } + + [Fact] + public void ExceptionThrownWhenWrongPropertyChanged() + { + var obj = new NotifiedClass(); + + var ex = Record.Exception(() => Assert.PropertyChanged(obj, nameof(NotifiedClass.Property1), () => obj.Property2 = 42)); + + Assert.IsType(ex); + Assert.Equal("Assert.PropertyChanged() failure: Property 'Property1' was not set", ex.Message); + } + + [Fact] + public void NoExceptionThrownWhenPropertyChanged() + { + var obj = new NotifiedClass(); + + Assert.PropertyChanged(obj, nameof(NotifiedClass.Property1), () => obj.Property1 = "NewValue"); + } + + [Fact] + public void NoExceptionThrownWhenMultiplePropertyChangesIncludesCorrectProperty() + { + var obj = new NotifiedClass(); + + Assert.PropertyChanged(obj, nameof(NotifiedClass.Property1), () => + { + obj.Property2 = 12; + obj.Property1 = "New Value"; + obj.Property2 = 42; + }); + } + } + + public class PropertyChangedAsync + { + [Fact] + public async Task GuardClauses() + { + await Assert.ThrowsAsync("object", () => Assert.PropertyChangedAsync(null!, "propertyName", () => Task.FromResult(0))); + await Assert.ThrowsAsync("testCode", () => Assert.PropertyChangedAsync(new NotifiedClass(), "propertyName", default(Func)!)); + } + + [Fact] + public async Task ExceptionThrownWhenPropertyNotChanged_Task() + { + var obj = new NotifiedClass(); + + var ex = await Record.ExceptionAsync(() => Assert.PropertyChangedAsync(obj, nameof(NotifiedClass.Property1), () => Task.FromResult(0))); + + Assert.IsType(ex); + Assert.Equal("Assert.PropertyChanged() failure: Property 'Property1' was not set", ex.Message); + } + +#pragma warning disable CS1998 + [Fact] + public async Task ExceptionThrownWhenWrongPropertyChangedAsync_Task() + { + var obj = new NotifiedClass(); + async Task setter() => obj!.Property2 = 42; + + var ex = await Record.ExceptionAsync(() => Assert.PropertyChangedAsync(obj, nameof(NotifiedClass.Property1), setter)); + + Assert.IsType(ex); + Assert.Equal("Assert.PropertyChanged() failure: Property 'Property1' was not set", ex.Message); + } + + [Fact] + public async Task NoExceptionThrownWhenPropertyChangedAsync_Task() + { + var obj = new NotifiedClass(); + async Task setter() => obj!.Property1 = "NewValue"; + + await Assert.PropertyChangedAsync(obj, nameof(NotifiedClass.Property1), setter); + } + + [Fact] + public async Task NoExceptionThrownWhenMultiplePropertyChangesIncludesCorrectProperty_Task() + { + var obj = new NotifiedClass(); + + async Task setter() + { + obj.Property2 = 12; + obj.Property1 = "New Value"; + obj.Property2 = 42; + } + + await Assert.PropertyChangedAsync(obj, nameof(NotifiedClass.Property1), setter); + } +#pragma warning restore CS1998 + } + + class NotifiedClass : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + public string Property1 + { + set { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Property1))); } + } + + public int Property2 + { + set { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Property2))); } + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/RangeAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/RangeAssertsTests.cs new file mode 100644 index 00000000000..07ddb12139c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/RangeAssertsTests.cs @@ -0,0 +1,200 @@ +using Xunit; +using Xunit.Sdk; + +public class RangeAssertsTests +{ + public class InRange + { + [CulturedFactDefault] + public void DoubleNotWithinRange() + { + var ex = Record.Exception(() => Assert.InRange(1.50, .75, 1.25)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.InRange() Failure: Value not in range" + Environment.NewLine + + $"Range: ({0.75:G17} - {1.25:G17})" + Environment.NewLine + + $"Actual: {1.5:G17}", + ex.Message + ); + } + + [Fact] + public void DoubleValueWithinRange() + { + Assert.InRange(1.0, .75, 1.25); + } + + [Fact] + public void IntNotWithinRangeWithZeroActual() + { + var ex = Record.Exception(() => Assert.InRange(0, 1, 2)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.InRange() Failure: Value not in range" + Environment.NewLine + + "Range: (1 - 2)" + Environment.NewLine + + "Actual: 0", + ex.Message + ); + } + + [Fact] + public void IntNotWithinRangeWithZeroMinimum() + { + var ex = Record.Exception(() => Assert.InRange(2, 0, 1)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.InRange() Failure: Value not in range" + Environment.NewLine + + "Range: (0 - 1)" + Environment.NewLine + + "Actual: 2", + ex.Message + ); + } + + [Fact] + public void IntValueWithinRange() + { + Assert.InRange(2, 1, 3); + } + + [Fact] + public void StringNotWithinRange() + { + var ex = Record.Exception(() => Assert.InRange("adam", "bob", "scott")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.InRange() Failure: Value not in range" + Environment.NewLine + + "Range: (\"bob\" - \"scott\")" + Environment.NewLine + + "Actual: \"adam\"", + ex.Message + ); + } + + [Fact] + public void StringValueWithinRange() + { + Assert.InRange("bob", "adam", "scott"); + } + } + + public class InRange_WithComparer + { + [Fact] + public void DoubleValueWithinRange() + { + Assert.InRange(400.0, .75, 1.25, new DoubleComparer(-1)); + } + + [CulturedFactDefault] + public void DoubleValueNotWithinRange() + { + var ex = Record.Exception(() => Assert.InRange(1.0, .75, 1.25, new DoubleComparer(1))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.InRange() Failure: Value not in range" + Environment.NewLine + + $"Range: ({0.75:G17} - {1.25:G17})" + Environment.NewLine + + $"Actual: {1:G17}", + ex.Message + ); + } + } + + public class NotInRange + { + [Fact] + public void DoubleNotWithinRange() + { + Assert.NotInRange(1.50, .75, 1.25); + } + + [Fact] + public void DoubleWithinRange() + { + var ex = Record.Exception(() => Assert.NotInRange(1.0, .75, 1.25)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotInRange() Failure: Value in range" + Environment.NewLine + + "Range: (0.75 - 1.25)" + Environment.NewLine + + "Actual: 1", + ex.Message + ); + } + + [Fact] + public void IntNotWithinRange() + { + Assert.NotInRange(1, 2, 3); + } + + [Fact] + public void IntWithinRange() + { + var ex = Record.Exception(() => Assert.NotInRange(2, 1, 3)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotInRange() Failure: Value in range" + Environment.NewLine + + "Range: (1 - 3)" + Environment.NewLine + + "Actual: 2", + ex.Message + ); + } + + [Fact] + public void StringNotWithNotInRange() + { + Assert.NotInRange("adam", "bob", "scott"); + } + + [Fact] + public void StringWithNotInRange() + { + var ex = Record.Exception(() => Assert.NotInRange("bob", "adam", "scott")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotInRange() Failure: Value in range" + Environment.NewLine + + "Range: (\"adam\" - \"scott\")" + Environment.NewLine + + "Actual: \"bob\"", + ex.Message + ); + } + } + + public class NotInRange_WithComparer + { + [Fact] + public void DoubleValueWithinRange() + { + var ex = Record.Exception(() => Assert.NotInRange(400.0, .75, 1.25, new DoubleComparer(-1))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.NotInRange() Failure: Value in range" + Environment.NewLine + + "Range: (0.75 - 1.25)" + Environment.NewLine + + "Actual: 400", + ex.Message + ); + } + + [Fact] + public void DoubleValueNotWithinRange() + { + Assert.NotInRange(1.0, .75, 1.25, new DoubleComparer(1)); + } + } + + class DoubleComparer(int returnValue) : + IComparer + { + public int Compare( + double x, + double y) => + returnValue; + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/ArgumentFormatterTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/ArgumentFormatterTests.cs new file mode 100644 index 00000000000..617a91d71a3 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/ArgumentFormatterTests.cs @@ -0,0 +1,566 @@ +using System.Collections; +using Xunit; +using Xunit.Sdk; + +public class ArgumentFormatterTests +{ + public class SimpleValues + { + [CulturedFactDefault] + public static void NullValue() + { + Assert.Equal("null", ArgumentFormatter.Format(null)); + } + + // NOTE: It's important that this stays as MemberData + // instead of InlineData. String constants in attributes + // are stored as UTF-8 as IL, so since "\uD800" cannot + // be converted to valid UTF-8, it ends up getting corrupted + // and being stored as two characters instead. Thus, this test + // will fail: + + // [Theory] + // [InlineData("\uD800", 1)] + // public void HasLength(string s, int length) + // { + // Assert.Equal(length, s.Length); + // } + + // While this one will pass: + + // public static IEnumerable HasLength_TestData() + // { + // yield return new object[] { "\uD800", 1 }; + // } + + // [Theory] + // [MemberData(nameof(HasLength_TestData))] + // public void HasLength(string s, int length) + // { + // Assert.Equal(length, s.Length); + // } + + // For more information, see the following links: + // - http://stackoverflow.com/q/36104766/4077294 + // - http://codeblog.jonskeet.uk/2014/11/07/when-is-a-string-not-a-string/ + public static IEnumerable> StringValue_TestData = + [ + new("\uD800", @"""\xd800"""), + new("\uDC00", @"""\xdc00"""), + new("\uDC00\uD800", @"""\xdc00\xd800"""), + new("\uFFFD", "\"\uFFFD\""), + ]; + + [Theory] + [InlineData("Hello, world!", "\"Hello, world!\"")] + [InlineData(@"""", @"""\""""")] // quotes should be escaped + [InlineData("\uD800\uDFFF", "\"\uD800\uDFFF\"")] // valid surrogates should print normally + [InlineData("\uFFFE", @"""\xfffe""")] // same for U+FFFE + [InlineData("\uFFFF", @"""\xffff""")] // and U+FFFF, which are non-characters + [InlineData("\u001F", @"""\x1f""")] // non-escaped C0 controls should be 2 digits + // Other escape sequences + [InlineData("\0", @"""\0""")] // null + [InlineData("\r", @"""\r""")] // carriage return + [InlineData("\n", @"""\n""")] // line feed + [InlineData("\a", @"""\a""")] // alert + [InlineData("\b", @"""\b""")] // backspace + [InlineData("\\", @"""\\""")] // backslash + [InlineData("\v", @"""\v""")] // vertical tab + [InlineData("\t", @"""\t""")] // tab + [InlineData("\f", @"""\f""")] // formfeed + [InlineData("----|----1----|----2----|----3----|----4----|----5-", "\"----|----1----|----2----|----3----|----4----|----5\"$$ELLIPSIS$$")] // truncation + [MemberData(nameof(StringValue_TestData), DisableDiscoveryEnumeration = true)] + public static void StringValue(string value, string expected) + { + Assert.Equal(expected.Replace("$$ELLIPSIS$$", ArgumentFormatter.Ellipsis), ArgumentFormatter.Format(value)); + } + + public static IEnumerable> CharValue_TestData = + [ + new('\uD800', "0xd800"), + new('\uDC00', "0xdc00"), + new('\uFFFD', "'\uFFFD'"), + new('\uFFFE', "0xfffe"), + ]; + + [Theory] + + // Printable + [InlineData(' ', "' '")] + [InlineData('a', "'a'")] + [InlineData('1', "'1'")] + [InlineData('!', "'!'")] + + // Escape sequences + [InlineData('\t', @"'\t'")] // tab + [InlineData('\n', @"'\n'")] // newline + [InlineData('\'', @"'\''")] // single quote + [InlineData('\v', @"'\v'")] // vertical tab + [InlineData('\a', @"'\a'")] // alert + [InlineData('\\', @"'\\'")] // backslash + [InlineData('\b', @"'\b'")] // backspace + [InlineData('\r', @"'\r'")] // carriage return + [InlineData('\f', @"'\f'")] // formfeed + + // Non-ASCII + [InlineData('©', "'©'")] + [InlineData('╬', "'╬'")] + [InlineData('ئ', "'ئ'")] + + // Unprintable + [InlineData(char.MinValue, @"'\0'")] + [InlineData(char.MaxValue, "0xffff")] + [MemberData(nameof(CharValue_TestData), DisableDiscoveryEnumeration = true)] + public static void CharacterValue(char value, string expected) + { + Assert.Equal(expected, ArgumentFormatter.Format(value)); + } + + [CulturedFactDefault] + public static void FloatValue() + { + var floatPI = (float)Math.PI; + + Assert.Equal(floatPI.ToString("G9"), ArgumentFormatter.Format(floatPI)); + } + + [CulturedFactDefault] + public static void DoubleValue() + { + Assert.Equal(Math.PI.ToString("G17"), ArgumentFormatter.Format(Math.PI)); + } + + [CulturedFactDefault] + public static void DecimalValue() + { + Assert.Equal(123.45M.ToString(), ArgumentFormatter.Format(123.45M)); + } + + [CulturedFactDefault] + public static void DateTimeValue() + { + var now = DateTime.UtcNow; + + Assert.Equal(now.ToString("o"), ArgumentFormatter.Format(now)); + } + + [CulturedFactDefault] + public static void DateTimeOffsetValue() + { + var now = DateTimeOffset.UtcNow; + + Assert.Equal(now.ToString("o"), ArgumentFormatter.Format(now)); + } + + [Fact] + public static async Task TaskValue() + { + var task = Task.Run(() => { }, TestContext.Current.CancellationToken); + await task; + + Assert.Equal("Task { Status = RanToCompletion }", ArgumentFormatter.Format(task)); + } + + [Fact] + public static void TaskGenericValue() + { + var taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetException(new DivideByZeroException()); + + Assert.Equal("Task { Status = Faulted }", ArgumentFormatter.Format(taskCompletionSource.Task)); + } + + public static IEnumerable> TypeValueData = + [ + new(typeof(string), "typeof(string)"), + new(typeof(int[]), "typeof(int[])"), +#if !XUNIT_AOT // MakeArrayType is not available in Native AOT + new(typeof(int).MakeArrayType(1), "typeof(int[*])"), + new(typeof(int).MakeArrayType(2), "typeof(int[,])"), + new(typeof(int).MakeArrayType(3), "typeof(int[,,])"), +#endif + new(typeof(DateTime[,]), "typeof(System.DateTime[,])"), + new(typeof(decimal[][,]), "typeof(decimal[][,])"), + new(typeof(IEnumerable<>), "typeof(System.Collections.Generic.IEnumerable<>)"), + new(typeof(IEnumerable), "typeof(System.Collections.Generic.IEnumerable)"), + new(typeof(IDictionary<,>), "typeof(System.Collections.Generic.IDictionary<,>)"), + new(typeof(IDictionary), "typeof(System.Collections.Generic.IDictionary)"), + new(typeof(IDictionary), "typeof(System.Collections.Generic.IDictionary)"), + new(typeof(bool?), "typeof(bool?)"), + new(typeof(bool?[]), "typeof(bool?[])"), + new(typeof(nint), "typeof(nint)"), + new(typeof(IntPtr), "typeof(nint)"), + new(typeof(nuint), "typeof(nuint)"), + new(typeof(UIntPtr), "typeof(nuint)"), + ]; + + [Theory] + [MemberData(nameof(TypeValueData), DisableDiscoveryEnumeration = true)] + public static void TypeValue(Type type, string expected) + { + Assert.Equal(expected, ArgumentFormatter.Format(type)); + } + } + + public class Enums + { + public enum NonFlagsEnum + { + Value0 = 0, + Value1 = 1 + } + + [CulturedTheoryDefault] +#pragma warning disable xUnit1010 // The value is not convertible to the method parameter type + [InlineData(0, "Value0")] + [InlineData(1, "Value1")] + [InlineData(42, "42")] +#pragma warning restore xUnit1010 // The value is not convertible to the method parameter type + public static void NonFlags(NonFlagsEnum enumValue, string expected) + { + var actual = ArgumentFormatter.Format(enumValue); + + Assert.Equal(expected, actual); + } + + [Flags] + public enum FlagsEnum + { + Nothing = 0, + Value1 = 1, + Value2 = 2, + } + + [CulturedTheoryDefault] +#pragma warning disable xUnit1010 // The value is not convertible to the method parameter type + [InlineData(0, "Nothing")] + [InlineData(1, "Value1")] + [InlineData(3, "Value1 | Value2")] + // This is expected, not "Value1 | Value2 | 4" + [InlineData(7, "7")] +#pragma warning restore xUnit1010 // The value is not convertible to the method parameter type + public static void Flags(FlagsEnum enumValue, string expected) + { + var actual = ArgumentFormatter.Format(enumValue); + + Assert.Equal(expected, actual); + } + } + + public class KeyValuePair + { + [CulturedFactDefault] + public static void KeyValuePairValue() + { + var kvp = new KeyValuePair>(42, [21.12M, "2600"]); + var expected = $"[42] = [{21.12M}, \"2600\"]"; + + Assert.Equal(expected, ArgumentFormatter.Format(kvp)); + } + } + + public class Enumerables + { +#pragma warning disable xUnit1047 // Avoid using TheoryDataRow arguments that might not be serializable + + // Both tracked and untracked should be the same + public static TheoryData> Collections = + [ + new([1, 2.3M, "Hello, world!"]), + new(CollectionTracker.Wrap([1, 2.3M, "Hello, world!"])), + ]; + +#pragma warning restore xUnit1047 + + [CulturedTheoryDefault] + [MemberData(nameof(Collections), DisableDiscoveryEnumeration = true)] + public static void EnumerableValue(IEnumerable collection) + { + var expected = $"[1, {2.3M}, \"Hello, world!\"]"; + + Assert.Equal(expected, ArgumentFormatter.Format(collection)); + } + + [CulturedFactDefault] + public static void DictionaryValue() + { + var value = new Dictionary> + { + { 42, new() { 21.12M, "2600" } }, + { "123", new() { } }, + }; +#if XUNIT_AOT + var expected = "[[42, System.Collections.Generic.List`1[System.Object]], [123, System.Collections.Generic.List`1[System.Object]]]"; +#else + var expected = $"[[42] = [{21.12M}, \"2600\"], [\"123\"] = []]"; +#endif + + Assert.Equal(expected, ArgumentFormatter.Format(value)); + } + +#pragma warning disable xUnit1047 // Avoid using TheoryDataRow arguments that might not be serializable + + public static TheoryData> LongCollections = + [ + new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + new(CollectionTracker.Wrap([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])), + ]; + +#pragma warning restore xUnit1047 + + [CulturedTheoryDefault] + [MemberData(nameof(LongCollections), DisableDiscoveryEnumeration = true)] + public static void OnlyFirstFewValuesOfEnumerableAreRendered(IEnumerable collection) + { + Assert.Equal($"[0, 1, 2, 3, 4, {ArgumentFormatter.Ellipsis}]", ArgumentFormatter.Format(collection)); + } + + [CulturedFactDefault] + public static void EnumerablesAreRenderedWithMaximumDepthToPreventInfiniteRecursion() + { + var looping = new object[2]; + looping[0] = 42; + looping[1] = looping; + + Assert.Equal($"[42, [42, [42, [{ArgumentFormatter.Ellipsis}]]]]", ArgumentFormatter.Format(looping)); + } + + [Fact] + public static void GroupingIsRenderedAsCollectionsOfKeysLinkedToCollectionsOfValues() + { +#if XUNIT_AOT + var expected = $"[{ArgumentFormatter.Ellipsis}]"; +#else + var expected = "[True] = [0, 2, 4, 6, 8]"; +#endif + + var grouping = Enumerable.Range(0, 10).GroupBy(i => i % 2 == 0).FirstOrDefault(g => g.Key == true); + + Assert.Equal(expected, ArgumentFormatter.Format(grouping)); + } + + [Fact] + public static void GroupingsAreRenderedAsCollectionsOfKeysLinkedToCollectionsOfValues() + { +#if XUNIT_AOT + var expected = $"[{ArgumentFormatter.Ellipsis}]"; +#else + var expected = "[[True] = [0, 2, 4, 6, 8], [False] = [1, 3, 5, 7, 9]]"; +#endif + + var grouping = Enumerable.Range(0, 10).GroupBy(i => i % 2 == 0); + + Assert.Equal(expected, ArgumentFormatter.Format(grouping)); + } + } + + public class ComplexTypes + { + [CulturedFactDefault] + public static void Empty() + { +#if XUNIT_AOT + var expected = $"Object {{ {ArgumentFormatter.Ellipsis} }}"; +#else + var expected = "Object { }"; +#endif + + var result = ArgumentFormatter.Format(new object()); + + Assert.Equal(expected, result); + } + +#if !XUNIT_AOT // Native AOT cannot render properties and fields of object + + [CulturedFactDefault] + public static void ReturnsValuesInAlphabeticalOrder() + { + var expected = $"MyComplexType {{ MyPublicField = 42, MyPublicProperty = {21.12M} }}"; + + var result = ArgumentFormatter.Format(new MyComplexType()); + + Assert.Equal(expected, result); + } + + public class MyComplexType + { +#pragma warning disable CS0414 + readonly string MyPrivateField = "Hello, world"; +#pragma warning restore CS0414 + + public static int MyPublicStaticField = 2112; + + public decimal MyPublicProperty { get; private set; } + + public int MyPublicField = 42; + + public MyComplexType() + { + MyPublicProperty = 21.12M; + } + } + + [CulturedFactDefault] + public static void ComplexTypeInsideComplexType() + { + var expected = $"MyComplexTypeWrapper {{ c = 'A', s = \"Hello, world!\", t = MyComplexType {{ MyPublicField = 42, MyPublicProperty = {21.12M} }} }}"; + + var result = ArgumentFormatter.Format(new MyComplexTypeWrapper()); + + Assert.Equal(expected, result); + } + + public class MyComplexTypeWrapper + { + public MyComplexType t = new(); + public char c = 'A'; + public string s = "Hello, world!"; + } + + [CulturedFactDefault] + public static void WithThrowingPropertyGetter() + { + var result = ArgumentFormatter.Format(new ThrowingGetter()); + + Assert.Equal("ThrowingGetter { MyThrowingProperty = (throws NotImplementedException) }", result); + } + + public class ThrowingGetter + { + public string MyThrowingProperty { get { throw new NotImplementedException(); } } + } + + [CulturedFactDefault] + public static void LimitsOutputToFirstFewValues() + { + var expected = $@"Big {{ MyField1 = 42, MyField2 = ""Hello, world!"", MyProp1 = {21.12}, MyProp2 = typeof({typeof(Big).FullName}), MyProp3 = 2014-04-17T07:45:23.0000000+00:00, {ArgumentFormatter.Ellipsis} }}"; + + var result = ArgumentFormatter.Format(new Big()); + + Assert.Equal(expected, result); + } + + public class Big + { + public string MyField2 = "Hello, world!"; + + public decimal MyProp1 { get; set; } + + public object MyProp4 { get; set; } + + public object MyProp3 { get; set; } + + public int MyField1 = 42; + + public Type MyProp2 { get; set; } + + public Big() + { + MyProp1 = 21.12M; + MyProp2 = typeof(Big); + MyProp3 = new DateTimeOffset(2014, 04, 17, 07, 45, 23, TimeSpan.Zero); + MyProp4 = "Should not be shown"; + } + } + + [CulturedFactDefault] + public static void TypesAreRenderedWithMaximumDepthToPreventInfiniteRecursion() + { + var expected = $"Looping {{ Me = Looping {{ Me = Looping {{ {ArgumentFormatter.Ellipsis} }} }} }}"; + + var result = ArgumentFormatter.Format(new Looping()); + + Assert.Equal(expected, result); + } + + public class Looping + { + public Looping Me; + + public Looping() => Me = this; + } + + [CulturedFactDefault] + public static void RecursionViaCollectionsIsPreventedWithMaximumDepth() + { + var expected = $"LoopingChild {{ Parent = LoopingParent {{ ProjectsById = [{ArgumentFormatter.Ellipsis}] }} }}"; + + var result = ArgumentFormatter.Format(new LoopingChild(new LoopingParent())); + + Assert.Equal(expected, result); + } + + public class LoopingParent + { + public Dictionary ProjectsById { get; } = []; + } + + public class LoopingChild + { + public LoopingParent Parent; + + public LoopingChild(LoopingParent parent) + { + parent.ProjectsById[Guid.NewGuid().ToString()] = this; + Parent = parent; + } + } + +#endif // !XUNIT_AOT + + [Fact] + public static void WhenCustomTypeImplementsToString_UsesToString() + { + var result = ArgumentFormatter.Format(new TypeWithToString()); + + Assert.Equal("This is what you should show", result); + } + + public class TypeWithToString + { + public override string ToString() => "This is what you should show"; + } + } + + public class TypeNames + { + public static TheoryData ArgumentFormatterFormatTypeNamesData = new() + { + { typeof(int), "typeof(int)" }, + { typeof(long), "typeof(long)" }, + { typeof(string), "typeof(string)" }, + { typeof(List), "typeof(System.Collections.Generic.List)" }, + { typeof(Dictionary), "typeof(System.Collections.Generic.Dictionary)" }, + { typeof(List<>), "typeof(System.Collections.Generic.List<>)" }, + { typeof(Dictionary<,>), "typeof(System.Collections.Generic.Dictionary<,>)" } + }; + + [Theory] + [MemberData(nameof(ArgumentFormatterFormatTypeNamesData), DisableDiscoveryEnumeration = true)] + public void ArgumentFormatterFormatTypeNames(Type type, string expectedResult) + { + Assert.Equal(expectedResult, ArgumentFormatter.Format(type)); + } + + [Fact] + public void ArgumentFormatterFormatTypeNameGenericTypeParameter() + { + var genericTypeParameters = typeof(List<>).GetGenericArguments(); + var parameterType = genericTypeParameters.First(); + + Assert.Equal("typeof(T)", ArgumentFormatter.Format(parameterType)); + } + + [Fact] + public void ArgumentFormatterFormatTypeNameGenericTypeParameters() + { + var genericTypeParameters = typeof(Dictionary<,>).GetGenericArguments(); + var parameterTKey = genericTypeParameters.First(); + + Assert.Equal("typeof(TKey)", ArgumentFormatter.Format(parameterTKey)); + + var parameterTValue = genericTypeParameters.Last(); + Assert.Equal("typeof(TValue)", ArgumentFormatter.Format(parameterTValue)); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/AssertHelperTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/AssertHelperTests.cs new file mode 100644 index 00000000000..a30788c51ec --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/AssertHelperTests.cs @@ -0,0 +1,216 @@ +using System.Linq.Expressions; +using Xunit; + +public class AssertHelperTests +{ + public class ParseExclusionExpressions_LambdaExpression + { + [Fact] + public void NullExpression() + { + var expression = (Expression>)null!; + + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(expression)); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith("Null expression is not valid.", ex.Message); + } + + [Fact] + public void Properties() + { + var result = AssertHelper.ParseExclusionExpressions( + (Expression>)(c => c.Property), + (Expression>)(c => c.ParentProperty.Property), + (Expression>)(c => c.GrandparentProperty!.ParentProperty.Property) + ); + + Assert.Collection( + result, + ex => Assert.Equal((string.Empty, "Property"), ex), + ex => Assert.Equal(("ParentProperty", "Property"), ex), + ex => Assert.Equal(("GrandparentProperty.ParentProperty", "Property"), ex) + ); + } + + [Fact] + public void Fields() + { + var result = AssertHelper.ParseExclusionExpressions( + (Expression>)(c => c.Field), + (Expression>)(c => c.ParentField.Field), + (Expression>)(c => c.GrandparentField!.ParentField.Field) + ); + + Assert.Collection( + result, + ex => Assert.Equal((string.Empty, "Field"), ex), + ex => Assert.Equal(("ParentField", "Field"), ex), + ex => Assert.Equal(("GrandparentField.ParentField", "Field"), ex) + ); + } + + [Fact] + public void Mixed() + { + var result = AssertHelper.ParseExclusionExpressions( + (Expression>)(c => c.ParentProperty.Field), + (Expression>)(c => c.GrandparentProperty!.ParentField.Property) + ); + + Assert.Collection( + result, + ex => Assert.Equal(("ParentProperty", "Field"), ex), + ex => Assert.Equal(("GrandparentProperty.ParentField", "Property"), ex) + ); + } + + [Fact] + public void MethodNotSupported() + { + var expression = (Expression>)(c => c.Method()); + + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(expression)); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith($"Expression '{expression}' is not supported. Only property or field expressions from the lambda parameter are supported.", ex.Message); + } + + [Fact] + public void ChildMethodNotSupported() + { + var expression = (Expression>)(p => p.ParentProperty.Method()); + + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(expression)); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith($"Expression '{expression}' is not supported. Only property or field expressions from the lambda parameter are supported.", ex.Message); + } + + [Fact] + public void IndexerNotSupported() + { + var expression = (Expression>)(c => c[0]); + + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(expression)); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith($"Expression '{expression}' is not supported. Only property or field expressions from the lambda parameter are supported.", ex.Message); + } + + [Fact] + public void ChildIndexerNotSupported() + { + var expression = (Expression>)(p => p.ParentField[0]); + + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(expression)); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith($"Expression '{expression}' is not supported. Only property or field expressions from the lambda parameter are supported.", ex.Message); + } + + static readonly string foo = "Hello world"; + + [Fact] + public void ExpressionMustOriginateFromParameter() + { + var expression = (Expression>)(c => foo.Length); + + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(expression)); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith($"Expression '{expression}' is not supported. Only property or field expressions from the lambda parameter are supported.", ex.Message); + } + + class Child + { + public int Field = 2112; + + public string this[int idx] => idx.ToString(); + + public string? Property => ToString(); + + public string? Method() => ToString(); + } + + class Parent + { + public Child ParentField = new(); + + public Child ParentProperty { get; } = new(); + } + + class Grandparent + { + public Parent? GrandparentField = new(); + + public Parent? GrandparentProperty { get; set; } + } + } + + public class ParseExclusionExpressions_String + { + [Fact] + public void NullExpression() + { + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(default(string)!)); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith("Null/empty expressions are not valid.", ex.Message); + } + + [Fact] + public void EmptyExpression() + { + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(string.Empty)); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith("Null/empty expressions are not valid.", ex.Message); + } + + [Fact] + public void StartsWithDot() + { + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions(".Foo")); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith("Expression '.Foo' is not valid. Expressions may not start with a period.", ex.Message); + } + + [Fact] + public void EndsWithDot() + { + var ex = Record.Exception(() => AssertHelper.ParseExclusionExpressions("Foo.")); + + var argEx = Assert.IsType(ex); + Assert.Equal("exclusionExpressions", argEx.ParamName); + Assert.StartsWith("Expression 'Foo.' is not valid. Expressions may not end with a period.", ex.Message); + } + + [Fact] + public void SuccessCases() + { + var result = AssertHelper.ParseExclusionExpressions( + "Child", + "Parent.Child", + "Grandparent.Parent.Child" + ); + + Assert.Collection( + result, + ex => Assert.Equal((string.Empty, "Child"), ex), + ex => Assert.Equal(("Parent", "Child"), ex), + ex => Assert.Equal(("Grandparent.Parent", "Child"), ex) + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/CollectionTrackerTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/CollectionTrackerTests.cs new file mode 100644 index 00000000000..9cd6e544cef --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/Sdk/CollectionTrackerTests.cs @@ -0,0 +1,236 @@ +using Xunit; +using Xunit.Sdk; + +public class CollectionTrackerTests +{ + public class FormatIndexedMismatch_IEnumerable + { + [Fact] + public static void ExceededDepth() + { + var tracker = new[] { 42, 2112 }.AsTracker(); + + var result = tracker.FormatIndexedMismatch(2600, out var pointerIndent, ArgumentFormatter.MaxEnumerableLength + 1); + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}]", result); + // - ^ + Assert.Equal(1, pointerIndent); + } + + [Fact] + public static void SmallCollection_Start() + { + var tracker = new[] { 42, 2112 }.AsTracker(); + + var result = tracker.FormatIndexedMismatch(0, out var pointerIndent); + + Assert.Equal("[42, 2112]", result); + // -^ + Assert.Equal(1, pointerIndent); + } + + [Fact] + public static void LargeCollection_Start() + { + var tracker = new[] { 1, 2, 3, 4, 5, 6, 7 }.AsTracker(); + + var result = tracker.FormatIndexedMismatch(1, out var pointerIndent); + + Assert.Equal($"[1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]", result); + // ----^ + Assert.Equal(4, pointerIndent); + } + + [Fact] + public static void LargeCollection_Mid() + { + var tracker = new[] { 1, 2, 3, 4, 5, 6, 7 }.AsTracker(); + + var result = tracker.FormatIndexedMismatch(3, out var pointerIndent); + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}, 2, 3, 4, 5, 6, {ArgumentFormatter.Ellipsis}]", result); + // - --- |----|--^ + Assert.Equal(12, pointerIndent); + } + + [Fact] + public static void LargeCollection_End() + { + var tracker = new[] { 1, 2, 3, 4, 5, 6, 7 }.AsTracker(); + + var result = tracker.FormatIndexedMismatch(6, out var pointerIndent); + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}, 3, 4, 5, 6, 7]", result); + // - --- |----|----|---^ + Assert.Equal(18, pointerIndent); + } + } + + public class FormatIndexedMismatch_Span + { + [Fact] + public static void ExceededDepth() + { + var span = new[] { 42, 2112 }.AsSpan(); + + var result = CollectionTracker.FormatIndexedMismatch(span, 2600, out var pointerIndent, ArgumentFormatter.MaxEnumerableLength + 1); + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}]", result); + // - ^ + Assert.Equal(1, pointerIndent); + } + + [Fact] + public static void SmallCollection_Start() + { + var span = new[] { 42, 2112 }.AsSpan(); + + var result = CollectionTracker.FormatIndexedMismatch(span, 0, out var pointerIndent); + + Assert.Equal("[42, 2112]", result); + // -^ + Assert.Equal(1, pointerIndent); + } + + [Fact] + public static void LargeCollection_Start() + { + var span = new[] { 1, 2, 3, 4, 5, 6, 7 }.AsSpan(); + + var result = CollectionTracker.FormatIndexedMismatch(span, 1, out var pointerIndent); + + Assert.Equal($"[1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]", result); + // ----^ + Assert.Equal(4, pointerIndent); + } + + [Fact] + public static void LargeCollection_Mid() + { + var span = new[] { 1, 2, 3, 4, 5, 6, 7 }.AsSpan(); + + var result = CollectionTracker.FormatIndexedMismatch(span, 3, out var pointerIndent); + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}, 2, 3, 4, 5, 6, {ArgumentFormatter.Ellipsis}]", result); + // - --- |----|--^ + Assert.Equal(12, pointerIndent); + } + + [Fact] + public static void LargeCollection_End() + { + var span = new[] { 1, 2, 3, 4, 5, 6, 7 }.AsSpan(); + + var result = CollectionTracker.FormatIndexedMismatch(span, 6, out var pointerIndent); + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}, 3, 4, 5, 6, 7]", result); + // - --- |----|----|---^ + Assert.Equal(18, pointerIndent); + } + } + + public class FormatStart_IEnumerable_Tracked + { + [Fact] + public static void Empty() + { + var tracker = Array.Empty().AsTracker(); + + Assert.Equal("[]", tracker.FormatStart()); + } + + [Fact] + public static void ExceededDepth() + { + var tracker = Array.Empty().AsTracker(); + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}]", tracker.FormatStart(ArgumentFormatter.MaxEnumerableLength + 1)); + } + + [CulturedFactDefault] + public static void Short() + { + var tracker = new object[] { 1, 2.3M, "Hello, world!" }.AsTracker(); + + Assert.Equal($"[1, {2.3M}, \"Hello, world!\"]", tracker.FormatStart()); + } + + [CulturedFactDefault] + public static void Long() + { + var tracker = new object[] { 1, 2.3M, "Hello, world!", 42, 2112, new() }.AsTracker(); + + Assert.Equal($"[1, {2.3M}, \"Hello, world!\", 42, 2112, {ArgumentFormatter.Ellipsis}]", tracker.FormatStart()); + } + } + + public class FormatStart_IEnumerable_Untracked + { + [Fact] + public static void Empty() + { + IEnumerable collection = []; + + Assert.Equal("[]", CollectionTracker.FormatStart(collection)); + } + + [Fact] + public static void ExceededDepth() + { + IEnumerable collection = []; + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}]", CollectionTracker.FormatStart(collection, ArgumentFormatter.MaxEnumerableLength + 1)); + } + + [CulturedFactDefault] + public static void Short() + { + IEnumerable collection = [1, 2.3M, "Hello, world!"]; + + Assert.Equal($"[1, {2.3M}, \"Hello, world!\"]", CollectionTracker.FormatStart(collection)); + } + + [CulturedFactDefault] + public static void Long() + { + IEnumerable collection = [1, 2.3M, "Hello, world!", 42, 2112, new object()]; + + Assert.Equal($"[1, {2.3M}, \"Hello, world!\", 42, 2112, {ArgumentFormatter.Ellipsis}]", CollectionTracker.FormatStart(collection)); + } + } + + public class FormatStart_Span + { + [Fact] + public static void Empty() + { + var span = Array.Empty().AsSpan(); + + Assert.Equal("[]", CollectionTracker.FormatStart(span)); + } + + [Fact] + public static void ExceededDepth() + { + var span = Array.Empty().AsSpan(); + + Assert.Equal($"[{ArgumentFormatter.Ellipsis}]", CollectionTracker.FormatStart(span, ArgumentFormatter.MaxEnumerableLength + 1)); + } + + [CulturedFactDefault] + public static void Short() + { + var span = new object[] { 1, 2.3M, "Hello, world!" }.AsSpan(); + + Assert.Equal($"[1, {2.3M}, \"Hello, world!\"]", CollectionTracker.FormatStart(span)); + } + + [CulturedFactDefault] + public static void Long() + { + var span = new object[] { 1, 2.3M, "Hello, world!", 42, 2112, new() }.AsSpan(); + + Assert.Equal($"[1, {2.3M}, \"Hello, world!\", 42, 2112, {ArgumentFormatter.Ellipsis}]", CollectionTracker.FormatStart(span)); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/SetAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/SetAssertsTests.cs new file mode 100644 index 00000000000..2c61adefe63 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/SetAssertsTests.cs @@ -0,0 +1,345 @@ +using System.Collections.Immutable; +using Xunit; +using Xunit.Sdk; + +public class SetAssertsTests +{ + public class Contains + { + [Fact] + public static void ValueInSet() + { + var set = new HashSet(StringComparer.OrdinalIgnoreCase) { "forty-two" }; + + Assert.Contains("FORTY-two", set); + Assert.Contains("FORTY-two", (ISet)set); + Assert.Contains("FORTY-two", set.ToSortedSet(StringComparer.OrdinalIgnoreCase)); +#if NET8_0_OR_GREATER + Assert.Contains("FORTY-two", (IReadOnlySet)set); +#endif + Assert.Contains("FORTY-two", set.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase)); + Assert.Contains("FORTY-two", set.ToImmutableSortedSet(StringComparer.OrdinalIgnoreCase)); + } + + [Fact] + public static void ValueNotInSet() + { + var set = new HashSet() { "eleventeen" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Item not found in set" + Environment.NewLine + + "Set: [\"eleventeen\"]" + Environment.NewLine + + "Not found: \"FORTY-two\"", + ex.Message + ); + } + + assertFailure(() => Assert.Contains("FORTY-two", set)); + assertFailure(() => Assert.Contains("FORTY-two", (ISet)set)); + assertFailure(() => Assert.Contains("FORTY-two", set.ToSortedSet())); +#if NET8_0_OR_GREATER + assertFailure(() => Assert.Contains("FORTY-two", (IReadOnlySet)set)); +#endif + assertFailure(() => Assert.Contains("FORTY-two", set.ToImmutableHashSet())); + assertFailure(() => Assert.Contains("FORTY-two", set.ToImmutableSortedSet())); + } + } + + public class DoesNotContain + { + [Fact] + public static void ValueNotInSet() + { + var set = new HashSet() { "eleventeen" }; + + Assert.DoesNotContain("FORTY-two", set); + Assert.DoesNotContain("FORTY-two", (ISet)set); + Assert.DoesNotContain("FORTY-two", set.ToSortedSet()); +#if NET8_0_OR_GREATER + Assert.DoesNotContain("FORTY-two", (IReadOnlySet)set); +#endif + Assert.DoesNotContain("FORTY-two", set.ToImmutableHashSet()); + Assert.DoesNotContain("FORTY-two", set.ToImmutableSortedSet()); + } + + [Fact] + public static void ValueInSet() + { + var set = new HashSet(StringComparer.OrdinalIgnoreCase) { "forty-two" }; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Item found in set" + Environment.NewLine + + "Set: [\"forty-two\"]" + Environment.NewLine + + "Found: \"FORTY-two\"", + ex.Message + ); + } + + assertFailure(() => Assert.DoesNotContain("FORTY-two", set)); + assertFailure(() => Assert.DoesNotContain("FORTY-two", (ISet)set)); + assertFailure(() => Assert.DoesNotContain("FORTY-two", set.ToSortedSet(StringComparer.OrdinalIgnoreCase))); +#if NET8_0_OR_GREATER + assertFailure(() => Assert.DoesNotContain("FORTY-two", (IReadOnlySet)set)); +#endif + assertFailure(() => Assert.DoesNotContain("FORTY-two", set.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase))); + assertFailure(() => Assert.DoesNotContain("FORTY-two", set.ToImmutableSortedSet(StringComparer.OrdinalIgnoreCase))); + } + } + + public class ProperSubset + { + [Fact] + public static void GuardClause() + { + Assert.Throws("expectedSubset", () => Assert.ProperSubset(null!, new HashSet())); + } + + [Fact] + public static void IsSubsetButNotProperSubset() + { + var expectedSubset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 3 }; + + var ex = Record.Exception(() => Assert.ProperSubset(expectedSubset, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ProperSubset() Failure: Value is not a proper subset" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3]", + ex.Message + ); + } + + [Fact] + public static void IsProperSubset() + { + var expectedSubset = new HashSet { 1, 2, 3, 4 }; + var actual = new HashSet { 1, 2, 3 }; + + Assert.ProperSubset(expectedSubset, actual); + } + + [Fact] + public static void IsNotSubset() + { + var expectedSubset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 7 }; + + var ex = Record.Exception(() => Assert.ProperSubset(expectedSubset, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ProperSubset() Failure: Value is not a proper subset" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 7]", + ex.Message + ); + } + + [Fact] + public static void NullActual() + { + var ex = Record.Exception(() => Assert.ProperSubset(new HashSet(), null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ProperSubset() Failure: Value is not a proper subset" + Environment.NewLine + + "Expected: []" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + } + + public class ProperSuperset + { + [Fact] + public static void GuardClause() + { + Assert.Throws("expectedSuperset", () => Assert.ProperSuperset(null!, new HashSet())); + } + + [Fact] + public static void IsSupersetButNotProperSuperset() + { + var expectedSuperset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 3 }; + + var ex = Record.Exception(() => Assert.ProperSuperset(expectedSuperset, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ProperSuperset() Failure: Value is not a proper superset" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3]", + ex.Message + ); + } + + [Fact] + public static void IsProperSuperset() + { + var expectedSuperset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 3, 4 }; + + Assert.ProperSuperset(expectedSuperset, actual); + } + + [Fact] + public static void IsNotSuperset() + { + var expectedSuperset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 7 }; + + var ex = Record.Exception(() => Assert.ProperSuperset(expectedSuperset, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ProperSuperset() Failure: Value is not a proper superset" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 7]", + ex.Message + ); + } + + [Fact] + public void NullActual() + { + var ex = Record.Exception(() => Assert.ProperSuperset(new HashSet(), null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.ProperSuperset() Failure: Value is not a proper superset" + Environment.NewLine + + "Expected: []" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + } + + public class Subset + { + [Fact] + public static void GuardClause() + { + Assert.Throws("expectedSubset", () => Assert.Subset(null!, new HashSet())); + } + + [Fact] + public static void IsSubset() + { + var expectedSubset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 3 }; + + Assert.Subset(expectedSubset, actual); + } + + [Fact] + public static void IsProperSubset() + { + var expectedSubset = new HashSet { 1, 2, 3, 4 }; + var actual = new HashSet { 1, 2, 3 }; + + Assert.Subset(expectedSubset, actual); + } + + [Fact] + public static void IsNotSubset() + { + var expectedSubset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 7 }; + + var ex = Record.Exception(() => Assert.Subset(expectedSubset, actual)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Subset() Failure: Value is not a subset" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 7]", + ex.Message + ); + } + + [Fact] + public static void NullActual() + { + var ex = Record.Exception(() => Assert.Subset(new HashSet(), null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Subset() Failure: Value is not a subset" + Environment.NewLine + + "Expected: []" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + } + + public class Superset + { + [Fact] + public static void GuardClause() + { + Assert.Throws("expectedSuperset", () => Assert.Superset(null!, new HashSet())); + } + + [Fact] + public static void IsSuperset() + { + var expectedSuperset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 3 }; + + Assert.Superset(expectedSuperset, actual); + } + + [Fact] + public static void IsProperSuperset() + { + var expectedSuperset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 3, 4 }; + + Assert.Superset(expectedSuperset, actual); + } + + [Fact] + public static void IsNotSuperset() + { + var expectedSuperset = new HashSet { 1, 2, 3 }; + var actual = new HashSet { 1, 2, 7 }; + + var ex = Assert.Throws(() => Assert.Superset(expectedSuperset, actual)); + + Assert.Equal( + "Assert.Superset() Failure: Value is not a superset" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 7]", + ex.Message + ); + } + + [Fact] + public void NullActual() + { + var ex = Record.Exception(() => Assert.Superset(new HashSet(), null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Superset() Failure: Value is not a superset" + Environment.NewLine + + "Expected: []" + Environment.NewLine + + "Actual: null", + ex.Message + ); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/SpanAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/SpanAssertsTests.cs new file mode 100644 index 00000000000..8808af0265d --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/SpanAssertsTests.cs @@ -0,0 +1,948 @@ +using Xunit; +using Xunit.Sdk; + +public class SpanAssertsTests +{ + public class Contains + { + public class Strings + { + [Fact] + public void ReadOnlySpan_Success() + { + Assert.Contains("wor".AsSpan(), "Hello, world!".AsSpan()); + } + + [Fact] + public void ReadWriteSpan_Success() + { + Assert.Contains("wor".Spanify(), "Hello, world!".Spanify()); + } + + [Fact] + public void ReadOnlySpan_CaseSensitiveByDefault() + { + var ex = Record.Exception(() => Assert.Contains("WORLD".AsSpan(), "Hello, world!".AsSpan())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Not found: \"WORLD\"", + ex.Message + ); + } + + [Fact] + public void ReadWriteSpan_CaseSensitiveByDefault() + { + var ex = Record.Exception(() => Assert.Contains("WORLD".Spanify(), "Hello, world!".Spanify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Not found: \"WORLD\"", + ex.Message + ); + } + + [Fact] + public void ReadOnlySpan_CanSpecifyComparisonType() + { + Assert.Contains("WORLD".AsSpan(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ReadWriteSpan_CanSpecifyComparisonType() + { + Assert.Contains("WORLD".Spanify(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ReadOnlySpan_NullStringIsEmpty() + { + var ex = Record.Exception(() => Assert.Contains("foo".AsSpan(), default(string).AsSpan())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"\"" + Environment.NewLine + + "Not found: \"foo\"", + ex.Message + ); + } + + [Fact] + public void ReadWriteSpan_NullStringIsEmpty() + { + var ex = Record.Exception(() => Assert.Contains("foo".Spanify(), default(string).Spanify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"\"" + Environment.NewLine + + "Not found: \"foo\"", + ex.Message + ); + } + + [Fact] + public void VeryLongStrings() + { + var ex = Record.Exception( + () => Assert.Contains( + "We are looking for something very long as well".Spanify(), + "This is a relatively long string so that we can see the truncation in action".Spanify() + ) + ); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + $"String: \"This is a relatively long string so that we can se\"{ArgumentFormatter.Ellipsis}" + Environment.NewLine + + "Not found: \"We are looking for something very long as well\"", + ex.Message + ); + } + } + + public class NonStrings + { + [Fact] + public void ReadOnlySpanOfInts_Success() + { + Assert.Contains(new int[] { 3, 4 }.AsSpan(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.AsSpan()); + } + + [Fact] + public void ReadOnlySpanOfStrings_Success() + { + Assert.Contains(new string[] { "test", "it" }.AsSpan(), new string[] { "something", "interesting", "test", "it", "out" }.AsSpan()); + } + + [Fact] + public void ReadWriteSpanOfInts_Success() + { + Assert.Contains(new int[] { 3, 4 }.Spanify(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.Spanify()); + } + + [Fact] + public void ReadWriteSpanOfStrings_Success() + { + Assert.Contains(new string[] { "test", "it" }.Spanify(), new string[] { "something", "interesting", "test", "it", "out" }.Spanify()); + } + + [Fact] + public void ReadOnlySpanOfInts_Failure() + { + var ex = Record.Exception(() => Assert.Contains(new int[] { 13, 14 }.AsSpan(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.AsSpan())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-span not found" + Environment.NewLine + + $"Span: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Not found: [13, 14]", + ex.Message + ); + } + + [Fact] + public void ReadWriteSpanOfInts_Failure() + { + var ex = Record.Exception(() => Assert.Contains(new int[] { 13, 14 }.Spanify(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.Spanify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-span not found" + Environment.NewLine + + $"Span: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Not found: [13, 14]", + ex.Message + ); + } + + [Fact] + public void FindingNonEmptySpanInsideEmptySpanFails() + { + var ex = Record.Exception(() => Assert.Contains(new int[] { 3, 4 }.Spanify(), Span.Empty)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-span not found" + Environment.NewLine + + "Span: []" + Environment.NewLine + + "Not found: [3, 4]", + ex.Message + ); + } + + [Fact] + public void FindingEmptySpanInsideAnySpanSucceeds() + { + Assert.Contains(Span.Empty, new int[] { 3, 4 }.Spanify()); + Assert.Contains(Span.Empty, Span.Empty); + } + } + } + + public class DoesNotContain + { + public class Strings + { + [Fact] + public void ReadOnlySpan_Success() + { + Assert.DoesNotContain("hey".AsSpan(), "Hello, world!".AsSpan()); + } + + [Fact] + public void ReadWriteSpan_Success() + { + Assert.DoesNotContain("hey".Spanify(), "Hello, world!".Spanify()); + } + + [Fact] + public void ReadOnlySpan_CaseSensitiveByDefault() + { + Assert.DoesNotContain("WORLD".AsSpan(), "Hello, world!".AsSpan()); + } + + [Fact] + public void ReadWriteSpan_CaseSensitiveByDefault() + { + Assert.DoesNotContain("WORLD".Spanify(), "Hello, world!".Spanify()); + } + + [Fact] + public void ReadOnlySpan_CanSpecifyComparisonType() + { + var ex = Record.Exception(() => Assert.DoesNotContain("WORLD".AsSpan(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"WORLD\"", + ex.Message + ); + } + + [Fact] + public void ReadWriteSpan_CanSpecifyComparisonType() + { + var ex = Record.Exception(() => Assert.DoesNotContain("WORLD".Spanify(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"WORLD\"", + ex.Message + ); + } + + [Fact] + public void ReadOnlySpan_Failure() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".AsSpan(), "Hello, world!".AsSpan())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + [Fact] + public void ReadWriteSpan_Failure() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".Spanify(), "Hello, world!".Spanify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + [Fact] + public void ReadOnlySpan_NullStringIsEmpty() + { + Assert.DoesNotContain("foo".AsSpan(), default(string).AsSpan()); + } + + [Fact] + public void ReadWriteSpan_NullStringIsEmpty() + { + Assert.DoesNotContain("foo".Spanify(), default(string).AsSpan()); + } + + [Fact] + public void VeryLongString_FoundAtFront() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".AsSpan(), "Hello, world from a very long string that will end up being truncated".AsSpan())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + $"String: \"Hello, world from a very long string that will end\"{ArgumentFormatter.Ellipsis}" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + [Fact] + public void VeryLongString_FoundInMiddle() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".AsSpan(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".AsSpan())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 50)" + Environment.NewLine + + $"String: {ArgumentFormatter.Ellipsis}\" string that has 'Hello, world' placed in the midd\"{ArgumentFormatter.Ellipsis}" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + [Fact] + public void VeryLongString_FoundAtEnd() + { + var ex = Record.Exception(() => Assert.DoesNotContain("world".AsSpan(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".AsSpan())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 89)" + Environment.NewLine + + $"String: {ArgumentFormatter.Ellipsis}\"om the front truncated, just to say 'Hello, world'\"" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + } + + public class NonStrings + { + [Fact] + public void ReadOnlySpanOfInts_Success() + { + Assert.DoesNotContain(new int[] { 13, 14 }.AsSpan(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.AsSpan()); + } + + [Fact] + public void ReadOnlySpanOfStrings_Success() + { + Assert.DoesNotContain(new string[] { "it", "test" }.AsSpan(), new string[] { "something", "interesting", "test", "it", "out" }.AsSpan()); + } + + [Fact] + public void ReadWriteSpanOfInts_Success() + { + Assert.DoesNotContain(new int[] { 13, 14 }.Spanify(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.Spanify()); + } + + [Fact] + public void ReadWriteSpanOfStrings_Success() + { + Assert.DoesNotContain(new string[] { "it", "test" }.Spanify(), new string[] { "something", "interesting", "test", "it", "out" }.Spanify()); + } + + [Fact] + public void ReadOnlySpanOfInts_Failure() + { + var ex = Record.Exception(() => Assert.DoesNotContain(new int[] { 3, 4 }.AsSpan(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.AsSpan())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-span found" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + $"Span: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Found: [3, 4]", + ex.Message + ); + } + + [Fact] + public void ReadWriteSpanOfInts_Failure() + { + var ex = Record.Exception(() => Assert.DoesNotContain(new int[] { 3, 4 }.Spanify(), new int[] { 1, 2, 3, 4, 5, 6, 7 }.Spanify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-span found" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + $"Span: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine + + "Found: [3, 4]", + ex.Message + ); + } + + [Fact] + public void FindingNonEmptySpanInsideEmptySpanSucceeds() + { + Assert.DoesNotContain(new int[] { 3, 4 }.Spanify(), Span.Empty); + } + + [Theory] + [InlineData(new[] { 3, 4 })] + [InlineData(new int[0])] + public void FindingEmptySpanInsideAnySpanFails(IEnumerable data) + { + var ex = Record.Exception(() => Assert.DoesNotContain(Span.Empty, data.ToArray().Spanify())); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-span found" + Environment.NewLine + + (data.Any() ? " ↓ (pos 0)" + Environment.NewLine : "") + + "Span: " + CollectionTracker.FormatStart(data) + Environment.NewLine + + "Found: []", + ex.Message + ); + } + } + } + + public class EndsWith + { + [Fact] + public void Success() + { + Assert.EndsWith("world!".AsSpan(), "Hello, world!".AsSpan()); + Assert.EndsWith("world!".AsSpan(), "Hello, world!".Spanify()); + Assert.EndsWith("world!".Spanify(), "Hello, world!".AsSpan()); + Assert.EndsWith("world!".Spanify(), "Hello, world!".Spanify()); + } + + [Fact] + public void Failure() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Expected end: \"hey\"", + ex.Message + ); + } + + assertFailure(() => Assert.EndsWith("hey".AsSpan(), "Hello, world!".AsSpan())); + assertFailure(() => Assert.EndsWith("hey".AsSpan(), "Hello, world!".Spanify())); + assertFailure(() => Assert.EndsWith("hey".Spanify(), "Hello, world!".AsSpan())); + assertFailure(() => Assert.EndsWith("hey".Spanify(), "Hello, world!".Spanify())); + } + + [Fact] + public void CaseSensitiveByDefault() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: \"world!\"" + Environment.NewLine + + "Expected end: \"WORLD!\"", + ex.Message + ); + } + + assertFailure(() => Assert.EndsWith("WORLD!".AsSpan(), "world!".AsSpan())); + assertFailure(() => Assert.EndsWith("WORLD!".AsSpan(), "world!".Spanify())); + assertFailure(() => Assert.EndsWith("WORLD!".Spanify(), "world!".AsSpan())); + assertFailure(() => Assert.EndsWith("WORLD!".Spanify(), "world!".Spanify())); + } + + [Fact] + public void CanSpecifyComparisonType() + { + Assert.EndsWith("WORLD!".AsSpan(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".AsSpan(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".Spanify(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".Spanify(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NullStringIsEmpty() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: \"\"" + Environment.NewLine + + "Expected end: \"foo\"", + ex.Message + ); + } + + assertFailure(() => Assert.EndsWith("foo".AsSpan(), null)); + assertFailure(() => Assert.EndsWith("foo".Spanify(), null)); + } + + [Fact] + public void Truncation() + { + var expected = "This is a long string that we're looking for at the end"; + var actual = "This is the long string that we expected to find this ending inside"; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: " + ArgumentFormatter.Ellipsis + "\"string that we expected to find this ending inside\"" + Environment.NewLine + + "Expected end: \"This is a long string that we're looking for at th\"" + ArgumentFormatter.Ellipsis, + ex.Message + ); + } + + assertFailure(() => Assert.EndsWith(expected.AsSpan(), actual.AsSpan())); + assertFailure(() => Assert.EndsWith(expected.AsSpan(), actual.Spanify())); + assertFailure(() => Assert.EndsWith(expected.Spanify(), actual.AsSpan())); + assertFailure(() => Assert.EndsWith(expected.Spanify(), actual.Spanify())); + } + } + + public class Equal + { + public class Chars_TreatedLikeStrings + { + [Theory] + // Null values + [InlineData(null, null, false, false, false, false)] + // Null ReadOnlySpan acts like an empty string + [InlineData(null, "", false, false, false, false)] + [InlineData("", null, false, false, false, false)] + // Empty values + [InlineData("", "", false, false, false, false)] + // Identical values + [InlineData("foo", "foo", false, false, false, false)] + // Case differences + [InlineData("foo", "FoO", true, false, false, false)] + // Line ending differences + [InlineData("foo \r\n bar", "foo \r bar", false, true, false, false)] + [InlineData("foo \r\n bar", "foo \n bar", false, true, false, false)] + [InlineData("foo \n bar", "foo \r bar", false, true, false, false)] + // Whitespace differences + [InlineData(" ", "\t", false, false, true, false)] + [InlineData(" \t", "\t ", false, false, true, false)] + [InlineData(" ", "\t", false, false, true, false)] + [InlineData(" ", " \u180E", false, false, true, false)] + [InlineData(" \u180E", "\u180E ", false, false, true, false)] + [InlineData(" ", "\u180E", false, false, true, false)] + [InlineData(" ", " \u200B", false, false, true, false)] + [InlineData(" \u200B", "\u200B ", false, false, true, false)] + [InlineData(" ", "\u200B", false, false, true, false)] + [InlineData(" ", " \u200B\uFEFF", false, false, true, false)] + [InlineData(" \u180E", "\u200B\u202F\u1680\u180E ", false, false, true, false)] + [InlineData("\u2001\u2002\u2003\u2006\u2009 ", "\u200B", false, false, true, false)] + [InlineData("\u00A0\u200A\u2009\u2006\u2009 ", "\u200B", false, false, true, false)] + // The ogham space mark (\u1680) kind of looks like a faint dash, but Microsoft has put it + // inside the SpaceSeparator unicode category, so we also treat it as a space + [InlineData("\u2007\u2008\u1680\t\u0009\u3000 ", " ", false, false, true, false)] + [InlineData("\u1680", "\t", false, false, true, false)] + [InlineData("\u1680", " ", false, false, true, false)] + // All whitespace differences + [InlineData("", " ", false, false, false, true)] + [InlineData("", " ", false, false, true, true)] + [InlineData("", "\t", false, false, true, true)] + [InlineData("foobar", "foo bar", false, false, true, true)] + public void Success( + string? value1, + string? value2, + bool ignoreCase, + bool ignoreLineEndingDifferences, + bool ignoreWhiteSpaceDifferences, + bool ignoreAllWhiteSpace) + { + // Run them in both directions, as the values should be interchangeable when they're equal + + // ReadOnlySpan vs. ReadOnlySpan + Assert.Equal(value1.AsSpan(), value2.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.AsSpan(), value1.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + // ReadOnlySpan vs. Span + Assert.Equal(value1.AsSpan(), value2.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.AsSpan(), value1.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + // Span vs. ReadOnlySpan + Assert.Equal(value1.Spanify(), value2.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.Spanify(), value1.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + // Span vs. Span + Assert.Equal(value1.Spanify(), value2.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.Spanify(), value1.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + } + + [Theory] + // Non-identical values + [InlineData("foo", "foo!", false, false, false, false, null, " ↑ (pos 3)")] + [InlineData("foo\0", "foo\0\0", false, false, false, false, null, " ↑ (pos 4)")] + // Overruns + [InlineData("first test", "first test 1", false, false, false, false, null, " ↑ (pos 10)")] + [InlineData("first test 1", "first test", false, false, false, false, " ↓ (pos 10)", null)] + // Case differences + [InlineData("Foobar", "foo bar", true, false, false, false, " ↓ (pos 3)", " ↑ (pos 3)")] + // Line ending differences + [InlineData("foo\nbar", "foo\rBar", false, true, false, false, " ↓ (pos 4)", " ↑ (pos 4)")] + // Non-zero whitespace quantity differences + [InlineData("foo bar", "foo Bar", false, false, true, false, " ↓ (pos 4)", " ↑ (pos 5)")] + // Ignore all white space differences + [InlineData("foobar", "foo Bar", false, false, false, true, " ↓ (pos 3)", " ↑ (pos 4)")] + public void Failure( + string expected, + string actual, + bool ignoreCase, + bool ignoreLineEndingDifferences, + bool ignoreWhiteSpaceDifferences, + bool ignoreAllWhiteSpace, + string? expectedPointer, + string? actualPointer) + { + var message = "Assert.Equal() Failure: Strings differ"; + + if (expectedPointer is not null) + message += Environment.NewLine + " " + expectedPointer; + + message += + Environment.NewLine + "Expected: " + ArgumentFormatter.Format(expected) + + Environment.NewLine + "Actual: " + ArgumentFormatter.Format(actual); + + if (actualPointer is not null) + message += Environment.NewLine + " " + actualPointer; + + void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + message, + ex.Message + ); + } + + assertFailure(() => Assert.Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + assertFailure(() => Assert.Equal(expected.Spanify(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + assertFailure(() => Assert.Equal(expected.AsSpan(), actual.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + assertFailure(() => Assert.Equal(expected.Spanify(), actual.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + } + + [Fact] + public void Truncation() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 21)" + Environment.NewLine + + $"Expected: \"Why hello there world, you're a long string with s\"{ArgumentFormatter.Ellipsis}" + Environment.NewLine + + "Actual: \"Why hello there world! You're a long string!\"" + Environment.NewLine + + " ↑ (pos 21)", + ex.Message + ); + } + + assertFailure( + () => Assert.Equal( + "Why hello there world, you're a long string with some truncation!".AsSpan(), + "Why hello there world! You're a long string!".AsSpan() + ) + ); + assertFailure( + () => Assert.Equal( + "Why hello there world, you're a long string with some truncation!".AsSpan(), + "Why hello there world! You're a long string!".Spanify() + ) + ); + assertFailure( + () => Assert.Equal( + "Why hello there world, you're a long string with some truncation!".Spanify(), + "Why hello there world! You're a long string!".AsSpan() + ) + ); + assertFailure( + () => Assert.Equal( + "Why hello there world, you're a long string with some truncation!".Spanify(), + "Why hello there world! You're a long string!".Spanify() + ) + ); + } + + // https://github.com/xunit/xunit/discussions/3021 + [Fact] + public void ArrayOverload() + { + string[] str = ["hello"]; + Assert.Equal(["hello"], str); + } + } + + public class Ints + { + [Theory] + // Null values + [InlineData(null, null)] + [InlineData(null, new int[] { })] // Null ReadOnlySpan acts like an empty array + [InlineData(new int[] { }, null)] + // Identical values + [InlineData(new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 })] + public void Success( + int[]? value1, + int[]? value2) + { + // Run them in both directions, as the values should be interchangeable when they're equal + + // ReadOnlySpan vs. ReadOnlySpan + Assert.Equal(value1.AsSpan(), value2.AsSpan()); + Assert.Equal(value2.AsSpan(), value1.AsSpan()); + + // ReadOnlySpan vs. Span + Assert.Equal(value1.AsSpan(), value2.Spanify()); + Assert.Equal(value2.AsSpan(), value1.Spanify()); + + // Span vs. ReadOnlySpan + Assert.Equal(value1.Spanify(), value2.AsSpan()); + Assert.Equal(value2.Spanify(), value1.AsSpan()); + + // Span vs. Span + Assert.Equal(value1.Spanify(), value2.Spanify()); + Assert.Equal(value2.Spanify(), value1.Spanify()); + } + + [Fact] + public void Failure_MidCollection() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + " ↓ (pos 1)" + Environment.NewLine + + "Expected: [1, 0, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3]" + Environment.NewLine + + " ↑ (pos 1)", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(new int[] { 1, 0, 2, 3 }.AsSpan(), new int[] { 1, 2, 3 }.AsSpan())); + assertFailure(() => Assert.Equal(new int[] { 1, 0, 2, 3 }.AsSpan(), new int[] { 1, 2, 3 }.Spanify())); + assertFailure(() => Assert.Equal(new int[] { 1, 0, 2, 3 }.Spanify(), new int[] { 1, 2, 3 }.AsSpan())); + assertFailure(() => Assert.Equal(new int[] { 1, 0, 2, 3 }.Spanify(), new int[] { 1, 2, 3 }.Spanify())); + } + + [Fact] + public void Failure_BeyondEnd() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: [1, 2, 3]" + Environment.NewLine + + "Actual: [1, 2, 3, 4]" + Environment.NewLine + + " ↑ (pos 3)", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(new int[] { 1, 2, 3 }.AsSpan(), new int[] { 1, 2, 3, 4 }.AsSpan())); + assertFailure(() => Assert.Equal(new int[] { 1, 2, 3 }.AsSpan(), new int[] { 1, 2, 3, 4 }.Spanify())); + assertFailure(() => Assert.Equal(new int[] { 1, 2, 3 }.Spanify(), new int[] { 1, 2, 3, 4 }.AsSpan())); + assertFailure(() => Assert.Equal(new int[] { 1, 2, 3 }.Spanify(), new int[] { 1, 2, 3, 4 }.Spanify())); + } + } + + public class Strings + { + [Theory] + // Null values + [InlineData(null, null)] + [InlineData(null, new string[] { })] // Null ReadOnlyMemory acts like an empty array + [InlineData(new string[] { }, null)] + // Identical values + [InlineData(new string[] { "yes", "no", "maybe" }, new string[] { "yes", "no", "maybe" })] + public void Success( + string[]? value1, + string[]? value2) + { + // Run them in both directions, as the values should be interchangeable when they're equal + + // ReadOnlyMemory vs. ReadOnlyMemory + Assert.Equal(value1.AsSpan(), value2.AsSpan()); + Assert.Equal(value2.AsSpan(), value1.AsSpan()); + + // ReadOnlyMemory vs. Memory + Assert.Equal(value1.AsSpan(), value2.Spanify()); + Assert.Equal(value2.AsSpan(), value1.Spanify()); + + // Memory vs. ReadOnlyMemory + Assert.Equal(value1.Spanify(), value2.AsSpan()); + Assert.Equal(value2.Spanify(), value1.AsSpan()); + + // Memory vs. Memory + Assert.Equal(value1.Spanify(), value2.Spanify()); + Assert.Equal(value2.Spanify(), value1.Spanify()); + } + + [Fact] + public void Failure() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Collections differ" + Environment.NewLine + + "Expected: [\"yes\", \"no\", \"maybe\"]" + Environment.NewLine + + "Actual: [\"yes\", \"no\", \"maybe\", \"so\"]" + Environment.NewLine + + " ↑ (pos 3)", + ex.Message + ); + } + + assertFailure(() => Assert.Equal(new string[] { "yes", "no", "maybe" }.AsSpan(), new string[] { "yes", "no", "maybe", "so" }.AsSpan())); + assertFailure(() => Assert.Equal(new string[] { "yes", "no", "maybe" }.AsSpan(), new string[] { "yes", "no", "maybe", "so" }.Spanify())); + assertFailure(() => Assert.Equal(new string[] { "yes", "no", "maybe" }.Spanify(), new string[] { "yes", "no", "maybe", "so" }.AsSpan())); + assertFailure(() => Assert.Equal(new string[] { "yes", "no", "maybe" }.Spanify(), new string[] { "yes", "no", "maybe", "so" }.Spanify())); + } + } + } + + public class StartsWith + { + [Fact] + public void Success() + { + Assert.StartsWith("Hello".AsSpan(), "Hello, world!".AsSpan()); + Assert.StartsWith("Hello".AsSpan(), "Hello, world!".Spanify()); + Assert.StartsWith("Hello".Spanify(), "Hello, world!".AsSpan()); + Assert.StartsWith("Hello".Spanify(), "Hello, world!".Spanify()); + } + + [Fact] + public void Failure() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Expected start: \"hey\"", + ex.Message + ); + } + + assertFailure(() => Assert.StartsWith("hey".AsSpan(), "Hello, world!".AsSpan())); + assertFailure(() => Assert.StartsWith("hey".AsSpan(), "Hello, world!".Spanify())); + assertFailure(() => Assert.StartsWith("hey".Spanify(), "Hello, world!".AsSpan())); + assertFailure(() => Assert.StartsWith("hey".Spanify(), "Hello, world!".Spanify())); + } + + [Fact] + public void CaseSensitiveByDefault() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"world!\"" + Environment.NewLine + + "Expected start: \"WORLD!\"", + ex.Message + ); + } + + assertFailure(() => Assert.StartsWith("WORLD!".AsSpan(), "world!".AsSpan())); + assertFailure(() => Assert.StartsWith("WORLD!".AsSpan(), "world!".Spanify())); + assertFailure(() => Assert.StartsWith("WORLD!".Spanify(), "world!".AsSpan())); + assertFailure(() => Assert.StartsWith("WORLD!".Spanify(), "world!".Spanify())); + } + + [Fact] + public void CanSpecifyComparisonType() + { + Assert.StartsWith("HELLO".AsSpan(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".AsSpan(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".Spanify(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".Spanify(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NullStringIsEmpty() + { + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"\"" + Environment.NewLine + + "Expected start: \"foo\"", + ex.Message + ); + } + + assertFailure(() => Assert.StartsWith("foo".AsSpan(), null)); + assertFailure(() => Assert.StartsWith("foo".Spanify(), null)); + } + + [Fact] + public void Truncation() + { + var expected = "This is a long string that we're looking for at the start"; + var actual = "This is the long string that we expected to find this starting inside"; + + static void assertFailure(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"This is the long string that we expected to find t\"" + ArgumentFormatter.Ellipsis + Environment.NewLine + + "Expected start: \"This is a long string that we're looking for at th\"" + ArgumentFormatter.Ellipsis, + ex.Message + ); + } + + assertFailure(() => Assert.StartsWith(expected.AsSpan(), actual.AsSpan())); + assertFailure(() => Assert.StartsWith(expected.AsSpan(), actual.Spanify())); + assertFailure(() => Assert.StartsWith(expected.Spanify(), actual.AsSpan())); + assertFailure(() => Assert.StartsWith(expected.Spanify(), actual.Spanify())); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/StringAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/StringAssertsTests.cs new file mode 100644 index 00000000000..7de6447a3c3 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/StringAssertsTests.cs @@ -0,0 +1,1167 @@ +#pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. + +using System.Text.RegularExpressions; +using Xunit; +using Xunit.Sdk; + +public class StringAssertsTests +{ + public class Contains + { + [Fact] + public void CanSearchForSubstrings() + { + Assert.Contains("wor", "Hello, world!"); + Assert.Contains("wor".Memoryify(), "Hello, world!".Memoryify()); + Assert.Contains("wor".AsMemory(), "Hello, world!".Memoryify()); + Assert.Contains("wor".Memoryify(), "Hello, world!".AsMemory()); + Assert.Contains("wor".AsMemory(), "Hello, world!".AsMemory()); + Assert.Contains("wor".Spanify(), "Hello, world!".Spanify()); + Assert.Contains("wor".AsSpan(), "Hello, world!".Spanify()); + Assert.Contains("wor".Spanify(), "Hello, world!".AsSpan()); + Assert.Contains("wor".AsSpan(), "Hello, world!".AsSpan()); + } + + [Fact] + public void SubstringContainsIsCaseSensitiveByDefault() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Not found: \"WORLD\"", + ex.Message + ); + } + + verify(() => Assert.Contains("WORLD", "Hello, world!")); + verify(() => Assert.Contains("WORLD".Memoryify(), "Hello, world!".Memoryify())); + verify(() => Assert.Contains("WORLD".AsMemory(), "Hello, world!".Memoryify())); + verify(() => Assert.Contains("WORLD".Memoryify(), "Hello, world!".AsMemory())); + verify(() => Assert.Contains("WORLD".AsMemory(), "Hello, world!".AsMemory())); + verify(() => Assert.Contains("WORLD".Spanify(), "Hello, world!".Spanify())); + verify(() => Assert.Contains("WORLD".AsSpan(), "Hello, world!".Spanify())); + verify(() => Assert.Contains("WORLD".Spanify(), "Hello, world!".AsSpan())); + verify(() => Assert.Contains("WORLD".AsSpan(), "Hello, world!".AsSpan())); + } + + [Fact] + public void SubstringNotFound() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Not found: \"hey\"", + ex.Message + ); + } + + verify(() => Assert.Contains("hey", "Hello, world!")); + verify(() => Assert.Contains("hey".Memoryify(), "Hello, world!".Memoryify())); + verify(() => Assert.Contains("hey".AsMemory(), "Hello, world!".Memoryify())); + verify(() => Assert.Contains("hey".Memoryify(), "Hello, world!".AsMemory())); + verify(() => Assert.Contains("hey".AsMemory(), "Hello, world!".AsMemory())); + verify(() => Assert.Contains("hey".Spanify(), "Hello, world!".Spanify())); + verify(() => Assert.Contains("hey".AsSpan(), "Hello, world!".Spanify())); + verify(() => Assert.Contains("hey".Spanify(), "Hello, world!".AsSpan())); + verify(() => Assert.Contains("hey".AsSpan(), "Hello, world!".AsSpan())); + } + + [Fact] + public void NullActualStringThrows() + { + var ex = Record.Exception(() => Assert.Contains("foo", default(string))); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + "String: null" + Environment.NewLine + + "Not found: \"foo\"", + ex.Message + ); + } + + [Fact] + public void VeryLongStrings() + { + var expected = "We are looking for something that is actually very long as well"; + var actual = "This is a relatively long string so that we can see the truncation in action"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + + $"String: \"This is a relatively long string so that we can se\"{ArgumentFormatter.Ellipsis}" + Environment.NewLine + + $"Not found: \"We are looking for something that is actually very\"{ArgumentFormatter.Ellipsis}", + ex.Message + ); + } + + verify(() => Assert.Contains(expected, actual)); + verify(() => Assert.Contains(expected.Memoryify(), actual.Memoryify())); + verify(() => Assert.Contains(expected.AsMemory(), actual.Memoryify())); + verify(() => Assert.Contains(expected.Memoryify(), actual.AsMemory())); + verify(() => Assert.Contains(expected.AsMemory(), actual.AsMemory())); + verify(() => Assert.Contains(expected.Spanify(), actual.Spanify())); + verify(() => Assert.Contains(expected.AsSpan(), actual.Spanify())); + verify(() => Assert.Contains(expected.Spanify(), actual.AsSpan())); + verify(() => Assert.Contains(expected.AsSpan(), actual.AsSpan())); + } + + [Fact] + public void CanSearchForSubstringsCaseInsensitive() + { + Assert.Contains("WORLD", "Hello, world!", StringComparison.OrdinalIgnoreCase); + Assert.Contains("WORLD".Memoryify(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + Assert.Contains("WORLD".AsMemory(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + Assert.Contains("WORLD".Memoryify(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.Contains("WORLD".AsMemory(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.Contains("WORLD".Spanify(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + Assert.Contains("WORLD".AsSpan(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + Assert.Contains("WORLD".Spanify(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + Assert.Contains("WORLD".AsSpan(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + } + } + + public class DoesNotContain + { + [Fact] + public void CanSearchForSubstrings() + { + Assert.DoesNotContain("hey", "Hello, world!"); + Assert.DoesNotContain("hey".Memoryify(), "Hello, world!".Memoryify()); + Assert.DoesNotContain("hey".AsMemory(), "Hello, world!".Memoryify()); + Assert.DoesNotContain("hey".Memoryify(), "Hello, world!".AsMemory()); + Assert.DoesNotContain("hey".AsMemory(), "Hello, world!".AsMemory()); + Assert.DoesNotContain("hey".Spanify(), "Hello, world!".Spanify()); + Assert.DoesNotContain("hey".AsSpan(), "Hello, world!".Spanify()); + Assert.DoesNotContain("hey".Spanify(), "Hello, world!".AsSpan()); + Assert.DoesNotContain("hey".AsSpan(), "Hello, world!".AsSpan()); + } + + [Fact] + public void SubstringDoesNotContainIsCaseSensitiveByDefault() + { + Assert.DoesNotContain("WORLD", "Hello, world!"); + Assert.DoesNotContain("WORLD".Memoryify(), "Hello, world!".Memoryify()); + Assert.DoesNotContain("WORLD".AsMemory(), "Hello, world!".Memoryify()); + Assert.DoesNotContain("WORLD".Memoryify(), "Hello, world!".AsMemory()); + Assert.DoesNotContain("WORLD".AsMemory(), "Hello, world!".AsMemory()); + Assert.DoesNotContain("WORLD".Spanify(), "Hello, world!".Spanify()); + Assert.DoesNotContain("WORLD".AsSpan(), "Hello, world!".Spanify()); + Assert.DoesNotContain("WORLD".Spanify(), "Hello, world!".AsSpan()); + Assert.DoesNotContain("WORLD".AsSpan(), "Hello, world!".AsSpan()); + } + + [Fact] + public void SubstringFound() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + verify(() => Assert.DoesNotContain("world", "Hello, world!")); + verify(() => Assert.DoesNotContain("world".Memoryify(), "Hello, world!".Memoryify())); + verify(() => Assert.DoesNotContain("world".AsMemory(), "Hello, world!".Memoryify())); + verify(() => Assert.DoesNotContain("world".Memoryify(), "Hello, world!".AsMemory())); + verify(() => Assert.DoesNotContain("world".AsMemory(), "Hello, world!".AsMemory())); + verify(() => Assert.DoesNotContain("world".Spanify(), "Hello, world!".Spanify())); + verify(() => Assert.DoesNotContain("world".AsSpan(), "Hello, world!".Spanify())); + verify(() => Assert.DoesNotContain("world".Spanify(), "Hello, world!".AsSpan())); + verify(() => Assert.DoesNotContain("world".AsSpan(), "Hello, world!".AsSpan())); + } + + [Fact] + public void NullActualStringDoesNotThrow() + { + Assert.DoesNotContain("foo", (string?)null); + } + + [Fact] + public void VeryLongString_FoundAtFront() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + $"String: \"Hello, world from a very long string that will end\"{ArgumentFormatter.Ellipsis}" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + verify(() => Assert.DoesNotContain("world", "Hello, world from a very long string that will end up being truncated")); + verify(() => Assert.DoesNotContain("world".Memoryify(), "Hello, world from a very long string that will end up being truncated".Memoryify())); + verify(() => Assert.DoesNotContain("world".AsMemory(), "Hello, world from a very long string that will end up being truncated".Memoryify())); + verify(() => Assert.DoesNotContain("world".Memoryify(), "Hello, world from a very long string that will end up being truncated".AsMemory())); + verify(() => Assert.DoesNotContain("world".AsMemory(), "Hello, world from a very long string that will end up being truncated".AsMemory())); + verify(() => Assert.DoesNotContain("world".Spanify(), "Hello, world from a very long string that will end up being truncated".Spanify())); + verify(() => Assert.DoesNotContain("world".AsSpan(), "Hello, world from a very long string that will end up being truncated".Spanify())); + verify(() => Assert.DoesNotContain("world".Spanify(), "Hello, world from a very long string that will end up being truncated".AsSpan())); + verify(() => Assert.DoesNotContain("world".AsSpan(), "Hello, world from a very long string that will end up being truncated".AsSpan())); + } + + [Fact] + public void VeryLongString_FoundInMiddle() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 50)" + Environment.NewLine + + $"String: {ArgumentFormatter.Ellipsis}\" string that has 'Hello, world' placed in the midd\"{ArgumentFormatter.Ellipsis}" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + verify(() => Assert.DoesNotContain("world", "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction")); + verify(() => Assert.DoesNotContain("world".Memoryify(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".Memoryify())); + verify(() => Assert.DoesNotContain("world".AsMemory(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".Memoryify())); + verify(() => Assert.DoesNotContain("world".Memoryify(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".AsMemory())); + verify(() => Assert.DoesNotContain("world".AsMemory(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".AsMemory())); + verify(() => Assert.DoesNotContain("world".Spanify(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".Spanify())); + verify(() => Assert.DoesNotContain("world".AsSpan(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".Spanify())); + verify(() => Assert.DoesNotContain("world".Spanify(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".AsSpan())); + verify(() => Assert.DoesNotContain("world".AsSpan(), "This is a relatively long string that has 'Hello, world' placed in the middle so that we can dual trunaction".AsSpan())); + } + + [Fact] + public void VeryLongString_FoundAtEnd() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 89)" + Environment.NewLine + + $"String: {ArgumentFormatter.Ellipsis}\"om the front truncated, just to say 'Hello, world'\"" + Environment.NewLine + + "Found: \"world\"", + ex.Message + ); + } + + verify(() => Assert.DoesNotContain("world", "This is a relatively long string that will from the front truncated, just to say 'Hello, world'")); + verify(() => Assert.DoesNotContain("world".Memoryify(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".Memoryify())); + verify(() => Assert.DoesNotContain("world".AsMemory(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".Memoryify())); + verify(() => Assert.DoesNotContain("world".Memoryify(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".AsMemory())); + verify(() => Assert.DoesNotContain("world".AsMemory(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".AsMemory())); + verify(() => Assert.DoesNotContain("world".Spanify(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".Spanify())); + verify(() => Assert.DoesNotContain("world".AsSpan(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".Spanify())); + verify(() => Assert.DoesNotContain("world".Spanify(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".AsSpan())); + verify(() => Assert.DoesNotContain("world".AsSpan(), "This is a relatively long string that will from the front truncated, just to say 'Hello, world'".AsSpan())); + } + + [Fact] + public void CanSearchForSubstringsCaseInsensitive() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + + " ↓ (pos 7)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Found: \"WORLD\"", + ex.Message + ); + } + + verify(() => Assert.DoesNotContain("WORLD", "Hello, world!", StringComparison.OrdinalIgnoreCase)); + verify(() => Assert.DoesNotContain("WORLD".Memoryify(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase)); + verify(() => Assert.DoesNotContain("WORLD".AsMemory(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase)); + verify(() => Assert.DoesNotContain("WORLD".Memoryify(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase)); + verify(() => Assert.DoesNotContain("WORLD".AsMemory(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase)); + verify(() => Assert.DoesNotContain("WORLD".Spanify(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase)); + verify(() => Assert.DoesNotContain("WORLD".AsSpan(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase)); + verify(() => Assert.DoesNotContain("WORLD".Spanify(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase)); + verify(() => Assert.DoesNotContain("WORLD".AsSpan(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase)); + } + } + + public class DoesNotMatch_Pattern + { + [Fact] + public void GuardClause() + { + Assert.Throws("expectedRegexPattern", () => Assert.DoesNotMatch((string?)null!, "Hello, world!")); + } + + [Fact] + public void Success() + { + Assert.DoesNotMatch(@"\d", "Hello"); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.DoesNotMatch("ll", "Hello, world!")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotMatch() Failure: Match found" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "RegEx: \"ll\"", + ex.Message + ); + } + } + + public class DoesNotMatch_Regex + { + [Fact] + public void GuardClause() + { + Assert.Throws("expectedRegex", () => Assert.DoesNotMatch((Regex?)null!, "Hello, world!")); + } + + [Fact] + public void Success() + { + Assert.DoesNotMatch(new Regex(@"\d"), "Hello"); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.DoesNotMatch(new Regex(@"ll"), "Hello, world!")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.DoesNotMatch() Failure: Match found" + Environment.NewLine + + " ↓ (pos 2)" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "RegEx: \"ll\"", + ex.Message + ); + } + } + + public class Empty + { + [Fact] + public static void GuardClause() + { + Assert.Throws("value", () => Assert.Empty(default!)); + } + + [Fact] + public static void EmptyString() + { + Assert.Empty(""); + } + + [Fact] + public static void NonEmptyString() + { + var ex = Record.Exception(() => Assert.Empty("Foo")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Empty() Failure: String was not empty" + Environment.NewLine + + "String: \"Foo\"", + ex.Message + ); + } + } + + public class EndsWith + { + [Fact] + public void Success() + { + Assert.EndsWith("world!", "Hello, world!"); + Assert.EndsWith("world!".Memoryify(), "Hello, world!".Memoryify()); + Assert.EndsWith("world!".AsMemory(), "Hello, world!".Memoryify()); + Assert.EndsWith("world!".Memoryify(), "Hello, world!".AsMemory()); + Assert.EndsWith("world!".AsMemory(), "Hello, world!".AsMemory()); + Assert.EndsWith("world!".Spanify(), "Hello, world!".Spanify()); + Assert.EndsWith("world!".AsSpan(), "Hello, world!".Spanify()); + Assert.EndsWith("world!".Spanify(), "Hello, world!".AsSpan()); + Assert.EndsWith("world!".AsSpan(), "Hello, world!".AsSpan()); + } + + [Fact] + public void Failure() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Expected end: \"hey\"", + ex.Message + ); + } + + verify(() => Assert.EndsWith("hey", "Hello, world!")); + verify(() => Assert.EndsWith("hey".Memoryify(), "Hello, world!".Memoryify())); + verify(() => Assert.EndsWith("hey".AsMemory(), "Hello, world!".Memoryify())); + verify(() => Assert.EndsWith("hey".Memoryify(), "Hello, world!".AsMemory())); + verify(() => Assert.EndsWith("hey".AsMemory(), "Hello, world!".AsMemory())); + verify(() => Assert.EndsWith("hey".Spanify(), "Hello, world!".Spanify())); + verify(() => Assert.EndsWith("hey".AsSpan(), "Hello, world!".Spanify())); + verify(() => Assert.EndsWith("hey".Spanify(), "Hello, world!".AsSpan())); + verify(() => Assert.EndsWith("hey".AsSpan(), "Hello, world!".AsSpan())); + } + + [Fact] + public void CaseSensitiveByDefault() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: \"world!\"" + Environment.NewLine + + "Expected end: \"WORLD!\"", + ex.Message + ); + } + + verify(() => Assert.EndsWith("WORLD!", "world!")); + verify(() => Assert.EndsWith("WORLD!".Memoryify(), "world!".Memoryify())); + verify(() => Assert.EndsWith("WORLD!".AsMemory(), "world!".Memoryify())); + verify(() => Assert.EndsWith("WORLD!".Memoryify(), "world!".AsMemory())); + verify(() => Assert.EndsWith("WORLD!".AsMemory(), "world!".AsMemory())); + verify(() => Assert.EndsWith("WORLD!".Spanify(), "world!".Spanify())); + verify(() => Assert.EndsWith("WORLD!".AsSpan(), "world!".Spanify())); + verify(() => Assert.EndsWith("WORLD!".Spanify(), "world!".AsSpan())); + verify(() => Assert.EndsWith("WORLD!".AsSpan(), "world!".AsSpan())); + } + + [Fact] + public void CanSpecifyComparisonType() + { + Assert.EndsWith("WORLD!", "Hello, world!", StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".Memoryify(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".AsMemory(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".Memoryify(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".AsMemory(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".Spanify(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".AsSpan(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".Spanify(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("WORLD!".AsSpan(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NullString() + { + var ex = Record.Exception(() => Assert.EndsWith("foo", null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: null" + Environment.NewLine + + "Expected end: \"foo\"", + ex.Message + ); + } + + [Fact] + public void Truncation() + { + var expected = "This is a long string that we're looking for at the end"; + var actual = "This is the long string that we expected to find this ending inside"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + + "String: " + ArgumentFormatter.Ellipsis + "\"string that we expected to find this ending inside\"" + Environment.NewLine + + "Expected end: \"This is a long string that we're looking for at th\"" + ArgumentFormatter.Ellipsis, + ex.Message + ); + } + + verify(() => Assert.EndsWith(expected, actual)); + verify(() => Assert.EndsWith(expected.Memoryify(), actual.Memoryify())); + verify(() => Assert.EndsWith(expected.AsMemory(), actual.Memoryify())); + verify(() => Assert.EndsWith(expected.Memoryify(), actual.AsMemory())); + verify(() => Assert.EndsWith(expected.AsMemory(), actual.AsMemory())); + verify(() => Assert.EndsWith(expected.Spanify(), actual.Spanify())); + verify(() => Assert.EndsWith(expected.AsSpan(), actual.Spanify())); + verify(() => Assert.EndsWith(expected.Spanify(), actual.AsSpan())); + verify(() => Assert.EndsWith(expected.AsSpan(), actual.AsSpan())); + } + } + + public class Equal + { + [Theory] + // Null values + [InlineData(null, null, false, false, false, false)] + // Empty values + [InlineData("", "", false, false, false, false)] + // Identical values + [InlineData("foo", "foo", false, false, false, false)] + // Case differences + [InlineData("foo", "FoO", true, false, false, false)] + // Line ending differences + [InlineData("foo \r\n bar", "foo \r bar", false, true, false, false)] + [InlineData("foo \r\n bar", "foo \n bar", false, true, false, false)] + [InlineData("foo \n bar", "foo \r bar", false, true, false, false)] + // Whitespace differences + [InlineData(" ", "\t", false, false, true, false)] + [InlineData(" \t", "\t ", false, false, true, false)] + [InlineData(" ", "\t", false, false, true, false)] + [InlineData(" ", " \u180E", false, false, true, false)] + [InlineData(" \u180E", "\u180E ", false, false, true, false)] + [InlineData(" ", "\u180E", false, false, true, false)] + [InlineData(" ", " \u200B", false, false, true, false)] + [InlineData(" \u200B", "\u200B ", false, false, true, false)] + [InlineData(" ", "\u200B", false, false, true, false)] + [InlineData(" ", " \u200B\uFEFF", false, false, true, false)] + [InlineData(" \u180E", "\u200B\u202F\u1680\u180E ", false, false, true, false)] + [InlineData("\u2001\u2002\u2003\u2006\u2009 ", "\u200B", false, false, true, false)] + [InlineData("\u00A0\u200A\u2009\u2006\u2009 ", "\u200B", false, false, true, false)] + // The ogham space mark (\u1680) kind of looks like a faint dash, but Microsoft has put it + // inside the SpaceSeparator unicode category, so we also treat it as a space + [InlineData("\u2007\u2008\u1680\t\u0009\u3000 ", " ", false, false, true, false)] + [InlineData("\u1680", "\t", false, false, true, false)] + [InlineData("\u1680", " ", false, false, true, false)] + // All whitespace differences + [InlineData("", " ", false, false, false, true)] + [InlineData("", " ", false, false, true, true)] + [InlineData("", "\t", false, false, true, true)] + [InlineData("foobar", "foo bar", false, false, true, true)] + public void Success( + string? value1, + string? value2, + bool ignoreCase, + bool ignoreLineEndingDifferences, + bool ignoreWhiteSpaceDifferences, + bool ignoreAllWhiteSpace) + { + // Run them in both directions, as the values should be interchangeable when they're equal + Assert.Equal(value1, value2, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2, value1, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value1.Memoryify(), value2.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.Memoryify(), value1.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value1.AsMemory(), value2.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.AsMemory(), value1.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value1.Memoryify(), value2.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.Memoryify(), value1.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value1.AsMemory(), value2.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.AsMemory(), value1.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value1.Spanify(), value2.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.Spanify(), value1.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value1.AsSpan(), value2.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.AsSpan(), value1.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value1.Spanify(), value2.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.Spanify(), value1.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value1.AsSpan(), value2.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + Assert.Equal(value2.AsSpan(), value1.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + } + + [Theory] + // Non-identical values + [InlineData("foo", "foo!", false, false, false, false, null, " ↑ (pos 3)")] + [InlineData("foo\0", "foo\0\0", false, false, false, false, null, " ↑ (pos 4)")] + // Nulls + [InlineData("first test 1", null, false, false, false, false, null, null)] + [InlineData(null, "first test 1", false, false, false, false, null, null)] + // Overruns + [InlineData("first test", "first test 1", false, false, false, false, null, " ↑ (pos 10)")] + [InlineData("first test 1", "first test", false, false, false, false, " ↓ (pos 10)", null)] + // Case differences + [InlineData("Foobar", "foo bar", true, false, false, false, " ↓ (pos 3)", " ↑ (pos 3)")] + // Line ending differences + [InlineData("foo\nbar", "foo\rBar", false, true, false, false, " ↓ (pos 4)", " ↑ (pos 4)")] + // Non-zero whitespace quantity differences + [InlineData("foo bar", "foo Bar", false, false, true, false, " ↓ (pos 4)", " ↑ (pos 5)")] + // Ignore all white space differences + [InlineData("foobar", "foo Bar", false, false, false, true, " ↓ (pos 3)", " ↑ (pos 4)")] + public void Failure( + string? expected, + string? actual, + bool ignoreCase, + bool ignoreLineEndingDifferences, + bool ignoreWhiteSpaceDifferences, + bool ignoreAllWhiteSpace, + string? expectedPointer, + string? actualPointer) + { + void verify(Action action) + { + var message = "Assert.Equal() Failure: Strings differ"; + + if (expectedPointer is not null) + message += Environment.NewLine + " " + expectedPointer; + + message += + Environment.NewLine + "Expected: " + ArgumentFormatter.Format(expected) + + Environment.NewLine + "Actual: " + ArgumentFormatter.Format(actual); + + if (actualPointer is not null) + message += Environment.NewLine + " " + actualPointer; + + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal(message, ex.Message); + } + + verify(() => Assert.Equal(expected, actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + if (expected is not null && actual is not null) + { + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace)); + } + } + + [Fact] + public void Truncation() + { + var expected = "Why hello there world, you're a long string with some truncation!"; + var actual = "Why hello there world! You're a long string!"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 21)" + Environment.NewLine + + $"Expected: \"Why hello there world, you're a long string with s\"{ArgumentFormatter.Ellipsis}" + Environment.NewLine + + "Actual: \"Why hello there world! You're a long string!\"" + Environment.NewLine + + " ↑ (pos 21)", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify())); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify())); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory())); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory())); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify())); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify())); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan())); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan())); + } + + [Fact] + public void Overrun_LongerExpected() + { + var expected = "012345678901234567890123456789012345678901234567890123456789"; + var actual = "01234567890123456789012345678901234567890123456789"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 50)" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Ellipsis}\"01234567890123456789012345678901234567890123456789\"" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Ellipsis}\"0123456789012345678901234567890123456789\"", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify())); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify())); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory())); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory())); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify())); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify())); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan())); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan())); + } + + [Fact] + public void Overrun_LongerExpected_IgnoreWhitespace() + { + var expected = " 012345678901234567890123456789012345678901234567890123456789"; + var actual = " 01234567890123456789012345678901234567890123456789"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 55)" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Ellipsis}\"01234567890123456789012345678901234567890123456789\"" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Ellipsis}\"0123456789012345678901234567890123456789\"", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual, ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan(), ignoreWhiteSpaceDifferences: true)); + } + + [Fact] + public void Overrun_LongerActual() + { + var expected = "01234567890123456789012345678901234567890123456789"; + var actual = "012345678901234567890123456789012345678901234567890123456789"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Ellipsis}\"0123456789012345678901234567890123456789\"" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Ellipsis}\"01234567890123456789012345678901234567890123456789\"" + Environment.NewLine + + " ↑ (pos 50)", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify())); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify())); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory())); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory())); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify())); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify())); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan())); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan())); + } + + [Fact] + public void Overrun_LongerActual_IgnoreWhitespace() + { + var expected = " 01234567890123456789012345678901234567890123456789"; + var actual = " 012345678901234567890123456789012345678901234567890123456789"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Ellipsis}\"23456789012345678901234567890123456789\"" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Ellipsis}\"234567890123456789012345678901234567890123456789\"" + Environment.NewLine + + " ↑ (pos 55)", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual, ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan(), ignoreWhiteSpaceDifferences: true)); + } + + [Fact] + public void MismatchedAtEnd_LongerExpected() + { + var expected = "012345678901234567890123456789012345678901234567890123456789"; + var actual = "01234567890123456789012345678901234567890123456789_1234"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 50)" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Ellipsis}\"01234567890123456789012345678901234567890123456789\"" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Ellipsis}\"0123456789012345678901234567890123456789_1234\"" + Environment.NewLine + + " ↑ (pos 50)", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify())); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify())); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory())); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory())); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify())); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify())); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan())); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan())); + } + + [Fact] + public void MismatchedAtEnd_LongerExpected_IgnoreWhitespace() + { + var expected = " 012345678901234567890123456789012345678901234567890123456789"; + var actual = " 01234567890123456789012345678901234567890123456789_1234"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 55)" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Ellipsis}\"01234567890123456789012345678901234567890123456789\"" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Ellipsis}\"0123456789012345678901234567890123456789_1234\"" + Environment.NewLine + + " ↑ (pos 53)", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual, ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan(), ignoreWhiteSpaceDifferences: true)); + } + + [Fact] + public void MismatchedAtEnd_LongerActual() + { + var expected = "01234567890123456789012345678901234567890123456789_1234"; + var actual = "012345678901234567890123456789012345678901234567890123456789"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 50)" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Ellipsis}\"0123456789012345678901234567890123456789_1234\"" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Ellipsis}\"01234567890123456789012345678901234567890123456789\"" + Environment.NewLine + + " ↑ (pos 50)", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify())); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify())); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory())); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory())); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify())); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify())); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan())); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan())); + } + + [Fact] + public void MismatchedAtEnd_LongerActual_IgnoreWhitespace() + { + var expected = " 01234567890123456789012345678901234567890123456789_1234"; + var actual = " 012345678901234567890123456789012345678901234567890123456789"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Equal() Failure: Strings differ" + Environment.NewLine + + " ↓ (pos 53)" + Environment.NewLine + + $"Expected: {ArgumentFormatter.Ellipsis}\"23456789012345678901234567890123456789_1234\"" + Environment.NewLine + + $"Actual: {ArgumentFormatter.Ellipsis}\"234567890123456789012345678901234567890123456789\"" + Environment.NewLine + + " ↑ (pos 55)", + ex.Message + ); + } + + verify(() => Assert.Equal(expected, actual, ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Memoryify(), actual.Memoryify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsMemory(), actual.Memoryify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Memoryify(), actual.AsMemory(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsMemory(), actual.AsMemory(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Spanify(), actual.Spanify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsSpan(), actual.Spanify(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.Spanify(), actual.AsSpan(), ignoreWhiteSpaceDifferences: true)); + verify(() => Assert.Equal(expected.AsSpan(), actual.AsSpan(), ignoreWhiteSpaceDifferences: true)); + } + } + + public class Matches_Pattern + { + [Fact] + public void GuardClause() + { + Assert.Throws("expectedRegexPattern", () => Assert.Matches((string?)null!, "Hello, world!")); + } + + [Fact] + public void Success() + { + Assert.Matches(@"\w", "Hello"); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Matches(@"\d+", "Hello, world!")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Matches() Failure: Pattern not found in value" + Environment.NewLine + + @"Regex: ""\\d+""" + Environment.NewLine + + @"Value: ""Hello, world!""", + ex.Message + ); + } + + [Fact] + public void Failure_NullActual() + { + var ex = Record.Exception(() => Assert.Matches(@"\d+", null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Matches() Failure: Pattern not found in value" + Environment.NewLine + + @"Regex: ""\\d+""" + Environment.NewLine + + "Value: null", + ex.Message + ); + } + } + + public class Matches_Regex + { + [Fact] + public void GuardClause() + { + Assert.Throws("expectedRegex", () => Assert.Matches((Regex?)null!, "Hello, world!")); + } + + [Fact] + public void Success() + { + Assert.Matches(new Regex(@"\w+"), "Hello"); + } + + [Fact] + public void UsesRegexOptions() + { + Assert.Matches(new Regex(@"[a-z]+", RegexOptions.IgnoreCase), "HELLO"); + } + + [Fact] + public void Failure() + { + var ex = Record.Exception(() => Assert.Matches(new Regex(@"\d+"), "Hello, world!")); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Matches() Failure: Pattern not found in value" + Environment.NewLine + + @"Regex: ""\\d+""" + Environment.NewLine + + @"Value: ""Hello, world!""", + ex.Message + ); + } + + [Fact] + public void Failure_NullActual() + { + var ex = Record.Exception(() => Assert.Matches(new Regex(@"\d+"), null)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.Matches() Failure: Pattern not found in value" + Environment.NewLine + + @"Regex: ""\\d+""" + Environment.NewLine + + "Value: null", + ex.Message + ); + } + } + + public class StartsWith + { + [Fact] + public void Success() + { + Assert.StartsWith("Hello", "Hello, world!"); + Assert.StartsWith("Hello".Memoryify(), "Hello, world!".Memoryify()); + Assert.StartsWith("Hello".AsMemory(), "Hello, world!".Memoryify()); + Assert.StartsWith("Hello".Memoryify(), "Hello, world!".AsMemory()); + Assert.StartsWith("Hello".AsMemory(), "Hello, world!".AsMemory()); + Assert.StartsWith("Hello".Spanify(), "Hello, world!".Spanify()); + Assert.StartsWith("Hello".AsSpan(), "Hello, world!".Spanify()); + Assert.StartsWith("Hello".Spanify(), "Hello, world!".AsSpan()); + Assert.StartsWith("Hello".AsSpan(), "Hello, world!".AsSpan()); + } + + [Fact] + public void Failure() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"Hello, world!\"" + Environment.NewLine + + "Expected start: \"hey\"", + ex.Message + ); + } + + verify(() => Assert.StartsWith("hey", "Hello, world!")); + verify(() => Assert.StartsWith("hey".Memoryify(), "Hello, world!".Memoryify())); + verify(() => Assert.StartsWith("hey".AsMemory(), "Hello, world!".Memoryify())); + verify(() => Assert.StartsWith("hey".Memoryify(), "Hello, world!".AsMemory())); + verify(() => Assert.StartsWith("hey".AsMemory(), "Hello, world!".AsMemory())); + verify(() => Assert.StartsWith("hey".Spanify(), "Hello, world!".Spanify())); + verify(() => Assert.StartsWith("hey".AsSpan(), "Hello, world!".Spanify())); + verify(() => Assert.StartsWith("hey".Spanify(), "Hello, world!".AsSpan())); + verify(() => Assert.StartsWith("hey".AsSpan(), "Hello, world!".AsSpan())); + } + + [Fact] + public void CaseSensitiveByDefault() + { + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"world!\"" + Environment.NewLine + + "Expected start: \"WORLD!\"", + ex.Message + ); + } + + verify(() => Assert.StartsWith("WORLD!", "world!")); + verify(() => Assert.StartsWith("WORLD!".Memoryify(), "world!".Memoryify())); + verify(() => Assert.StartsWith("WORLD!".AsMemory(), "world!".Memoryify())); + verify(() => Assert.StartsWith("WORLD!".Memoryify(), "world!".AsMemory())); + verify(() => Assert.StartsWith("WORLD!".AsMemory(), "world!".AsMemory())); + verify(() => Assert.StartsWith("WORLD!".Spanify(), "world!".Spanify())); + verify(() => Assert.StartsWith("WORLD!".AsSpan(), "world!".Spanify())); + verify(() => Assert.StartsWith("WORLD!".Spanify(), "world!".AsSpan())); + verify(() => Assert.StartsWith("WORLD!".AsSpan(), "world!".AsSpan())); + } + + [Fact] + public void CanSpecifyComparisonType() + { + Assert.StartsWith("HELLO", "Hello, world!", StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".Memoryify(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".AsMemory(), "Hello, world!".Memoryify(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".Memoryify(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".AsMemory(), "Hello, world!".AsMemory(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".Spanify(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".AsSpan(), "Hello, world!".Spanify(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".Spanify(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + Assert.StartsWith("HELLO".AsSpan(), "Hello, world!".AsSpan(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NullStrings() + { + var ex = Record.Exception(() => Assert.StartsWith(default(string), default)); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: null" + Environment.NewLine + + "Expected start: null", + ex.Message + ); + } + + [Fact] + public void Truncation() + { + var expected = "This is a long string that we're looking for at the start"; + var actual = "This is the long string that we expected to find this starting inside"; + + static void verify(Action action) + { + var ex = Record.Exception(action); + + Assert.IsType(ex); + Assert.Equal( + "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + + "String: \"This is the long string that we expected to find t\"" + ArgumentFormatter.Ellipsis + Environment.NewLine + + "Expected start: \"This is a long string that we're looking for at th\"" + ArgumentFormatter.Ellipsis, + ex.Message + ); + } + + verify(() => Assert.StartsWith(expected, actual)); + verify(() => Assert.StartsWith(expected.Memoryify(), actual.Memoryify())); + verify(() => Assert.StartsWith(expected.AsMemory(), actual.Memoryify())); + verify(() => Assert.StartsWith(expected.Memoryify(), actual.AsMemory())); + verify(() => Assert.StartsWith(expected.AsMemory(), actual.AsMemory())); + verify(() => Assert.StartsWith(expected.Spanify(), actual.Spanify())); + verify(() => Assert.StartsWith(expected.AsSpan(), actual.Spanify())); + verify(() => Assert.StartsWith(expected.Spanify(), actual.AsSpan())); + verify(() => Assert.StartsWith(expected.AsSpan(), actual.AsSpan())); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/TypeAssertsTests.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/TypeAssertsTests.cs new file mode 100644 index 00000000000..199db30ac5a --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Asserts/TypeAssertsTests.cs @@ -0,0 +1,778 @@ +using Xunit; +using Xunit.Sdk; + +#if NETFRAMEWORK +using System.Reflection; +using System.Xml; +#endif + +public class TypeAssertsTests +{ + +#pragma warning disable xUnit2032 // Type assertions based on 'assignable from' are confusingly named + + public class IsAssignableFrom_Generic + { + [Fact] + public void NullObject() + { + var result = Record.Exception(() => Assert.IsAssignableFrom(null)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsAssignableFrom() Failure: Value is null" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: null", + result.Message + ); + } + + [Fact] + public void SameType() + { + var ex = new InvalidCastException(); + + Assert.IsAssignableFrom(ex); + } + + [Fact] + public void BaseType() + { + var ex = new InvalidCastException(); + + Assert.IsAssignableFrom(ex); + } + + [Fact] + public void Interface() + { + var ex = new DisposableClass(); + + Assert.IsAssignableFrom(ex); + } + + [Fact] + public void ReturnsCastObject() + { + var ex = new InvalidCastException(); + + var result = Assert.IsAssignableFrom(ex); + + Assert.Same(ex, result); + } + + [Fact] + public void IncompatibleType() + { + var result = + Record.Exception( + () => Assert.IsAssignableFrom(new InvalidOperationException()) + ); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsAssignableFrom() Failure: Value is an incompatible type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidOperationException)", + result.Message + ); + } + } + +#pragma warning disable xUnit2007 // Do not use typeof expression to check the type + + public class IsAssignableFrom_NonGeneric + { + [Fact] + public void NullObject() + { + var result = Record.Exception(() => Assert.IsAssignableFrom(typeof(object), null)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsAssignableFrom() Failure: Value is null" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: null", + result.Message + ); + } + + [Fact] + public void SameType() + { + var ex = new InvalidCastException(); + + Assert.IsAssignableFrom(typeof(InvalidCastException), ex); + } + + [Fact] + public void BaseType() + { + var ex = new InvalidCastException(); + + Assert.IsAssignableFrom(typeof(Exception), ex); + } + + [Fact] + public void Interface() + { + var ex = new DisposableClass(); + + Assert.IsAssignableFrom(typeof(IDisposable), ex); + } + + [Fact] + public void ReturnsCastObject() + { + var ex = new InvalidCastException(); + + var result = Assert.IsAssignableFrom(ex); + + Assert.Same(ex, result); + } + + [Fact] + public void IncompatibleType() + { + var result = + Record.Exception( + () => Assert.IsAssignableFrom(typeof(InvalidCastException), new InvalidOperationException()) + ); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsAssignableFrom() Failure: Value is an incompatible type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidOperationException)", + result.Message + ); + } + } + +#pragma warning restore xUnit2007 // Do not use typeof expression to check the type + + public class IsNotAssignableFrom_Generic + { + [Fact] + public void NullObject() + { + Assert.IsNotAssignableFrom(null); + } + + [Fact] + public void SameType() + { + var ex = new InvalidCastException(); + + var result = Record.Exception(() => Assert.IsNotAssignableFrom(ex)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotAssignableFrom() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void BaseType() + { + var ex = new InvalidCastException(); + + var result = Record.Exception(() => Assert.IsNotAssignableFrom(ex)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotAssignableFrom() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.Exception)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void Interface() + { + var ex = new DisposableClass(); + + var result = Record.Exception(() => Assert.IsNotAssignableFrom(ex)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotAssignableFrom() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.IDisposable)" + Environment.NewLine + + "Actual: typeof(TypeAssertsTests+DisposableClass)", + result.Message + ); + } + + [Fact] + public void IncompatibleType() + { + Assert.IsNotAssignableFrom(new InvalidOperationException()); + } + } + + public class IsNotAssignableFrom_NonGeneric + { + [Fact] + public void NullObject() + { + Assert.IsNotAssignableFrom(typeof(object), null); + } + + [Fact] + public void SameType() + { + var ex = new InvalidCastException(); + + var result = Record.Exception(() => Assert.IsNotAssignableFrom(typeof(InvalidCastException), ex)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotAssignableFrom() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void BaseType() + { + var ex = new InvalidCastException(); + + var result = Record.Exception(() => Assert.IsNotAssignableFrom(typeof(Exception), ex)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotAssignableFrom() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.Exception)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void Interface() + { + var ex = new DisposableClass(); + + var result = Record.Exception(() => Assert.IsNotAssignableFrom(typeof(IDisposable), ex)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotAssignableFrom() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.IDisposable)" + Environment.NewLine + + "Actual: typeof(TypeAssertsTests+DisposableClass)", + result.Message + ); + } + + [Fact] + public void IncompatibleType() + { + Assert.IsNotAssignableFrom(typeof(InvalidCastException), new InvalidOperationException()); + } + } + +#pragma warning restore xUnit2032 // Type assertions based on 'assignable from' are confusingly named + + public class IsNotType_Generic + { + [Fact] + public void UnmatchedType() + { + var ex = new InvalidCastException(); + + Assert.IsNotType(ex); + } + + [Fact] + public void MatchedType() + { + var result = Record.Exception(() => Assert.IsNotType(new InvalidCastException())); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotType() Failure: Value is the exact type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void NullObject() + { + Assert.IsNotType(null); + } + } + + public class IsNotType_Generic_InexactMatch + { + [Fact] + public void NullObject() + { + Assert.IsNotType(null, exactMatch: false); + } + + [Fact] + public void SameType() + { + var ex = new InvalidCastException(); + + var result = Record.Exception(() => Assert.IsNotType(ex, exactMatch: false)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotType() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void BaseType() + { + var ex = new InvalidCastException(); + + var result = Record.Exception(() => Assert.IsNotType(ex, exactMatch: false)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotType() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.Exception)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void Interface() + { + var ex = new DisposableClass(); + +#pragma warning disable xUnit2018 // TODO: Temporary until xUnit2018 is updated for the new signatures + var result = Record.Exception(() => Assert.IsNotType(ex, exactMatch: false)); +#pragma warning restore xUnit2018 + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotType() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.IDisposable)" + Environment.NewLine + + "Actual: typeof(TypeAssertsTests+DisposableClass)", + result.Message + ); + } + + [Fact] + public void IncompatibleType() + { + Assert.IsNotType(new InvalidOperationException(), exactMatch: false); + } + } + +#pragma warning disable xUnit2007 // Do not use typeof expression to check the type + + public class IsNotType_NonGeneric + { + [Fact] + public void UnmatchedType() + { + var ex = new InvalidCastException(); + + Assert.IsNotType(typeof(Exception), ex); + } + + [Fact] + public void MatchedType() + { + var result = Record.Exception(() => Assert.IsNotType(typeof(InvalidCastException), new InvalidCastException())); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotType() Failure: Value is the exact type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void NullObject() + { + Assert.IsNotType(typeof(object), null); + } + } + + public class IsNotType_NonGeneric_InexactMatch + { + [Fact] + public void NullObject() + { + Assert.IsNotType(typeof(object), null, exactMatch: false); + } + + [Fact] + public void SameType() + { + var ex = new InvalidCastException(); + + var result = Record.Exception(() => Assert.IsNotType(typeof(InvalidCastException), ex, exactMatch: false)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotType() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void BaseType() + { + var ex = new InvalidCastException(); + + var result = Record.Exception(() => Assert.IsNotType(typeof(Exception), ex, exactMatch: false)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotType() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.Exception)" + Environment.NewLine + + "Actual: typeof(System.InvalidCastException)", + result.Message + ); + } + + [Fact] + public void Interface() + { + var ex = new DisposableClass(); + + var result = Record.Exception(() => Assert.IsNotType(typeof(IDisposable), ex, exactMatch: false)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsNotType() Failure: Value is a compatible type" + Environment.NewLine + + "Expected: typeof(System.IDisposable)" + Environment.NewLine + + "Actual: typeof(TypeAssertsTests+DisposableClass)", + result.Message + ); + } + + [Fact] + public void IncompatibleType() + { + Assert.IsNotType(typeof(InvalidCastException), new InvalidOperationException(), exactMatch: false); + } + } + +#pragma warning restore xUnit2007 // Do not use typeof expression to check the type + + public class IsType_Generic : TypeAssertsTests + { + [Fact] + public void MatchingType() + { + var ex = new InvalidCastException(); + + Assert.IsType(ex); + } + + [Fact] + public void ReturnsCastObject() + { + var ex = new InvalidCastException(); + + var result = Assert.IsType(ex); + + Assert.Same(ex, result); + } + + [Fact] + public void UnmatchedType() + { + var result = Record.Exception(() => Assert.IsType(new InvalidOperationException())); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is not the exact type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidOperationException)", + result.Message + ); + } + +#if NETFRAMEWORK + [Fact] + public async Task UnmatchedTypesWithIdenticalNamesShowAssemblies() + { + var dynamicAssembly = await CSharpDynamicAssembly.Create("namespace System.Xml { public class XmlException: Exception { } }"); + var assembly = Assembly.LoadFile(dynamicAssembly.FileName); + var dynamicXmlExceptionType = assembly.GetType("System.Xml.XmlException"); + Assert.NotNull(dynamicXmlExceptionType); + var ex = Activator.CreateInstance(dynamicXmlExceptionType); + + var result = Record.Exception(() => Assert.IsType(ex)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is not the exact type" + Environment.NewLine + + "Expected: typeof(System.Xml.XmlException) (from " + typeof(XmlException).Assembly.FullName + ")" + Environment.NewLine + + "Actual: typeof(System.Xml.XmlException) (from " + assembly.FullName + ")", + result.Message + ); + } +#endif + + [Fact] + public void NullObject() + { + var result = Record.Exception(() => Assert.IsType(null)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is null" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: null", + result.Message + ); + } + } + + public class IsType_Generic_InexactMatch + { + [Fact] + public void NullObject() + { + var result = Record.Exception(() => Assert.IsType(null, exactMatch: false)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is null" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: null", + result.Message + ); + } + + [Fact] + public void SameType() + { + var ex = new InvalidCastException(); + + Assert.IsType(ex, exactMatch: false); + } + + [Fact] + public void BaseType() + { + var ex = new InvalidCastException(); + + Assert.IsType(ex, exactMatch: false); + } + + [Fact] + public void Interface() + { + var ex = new DisposableClass(); + +#pragma warning disable xUnit2018 // TODO: Temporary until xUnit2018 is updated for the new signatures + Assert.IsType(ex, exactMatch: false); +#pragma warning restore xUnit2018 + } + + [Fact] + public void ReturnsCastObject() + { + var ex = new InvalidCastException(); + + var result = Assert.IsType(ex, exactMatch: false); + + Assert.Same(ex, result); + } + + [Fact] + public void IncompatibleType() + { + var result = + Record.Exception( + () => Assert.IsType(new InvalidOperationException(), exactMatch: false) + ); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is an incompatible type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidOperationException)", + result.Message + ); + } + } + +#pragma warning disable xUnit2007 // Do not use typeof expression to check the type + + public class IsType_NonGeneric : TypeAssertsTests + { + [Fact] + public void MatchingType() + { + var ex = new InvalidCastException(); + + Assert.IsType(typeof(InvalidCastException), ex); + } + + [Fact] + public void UnmatchedTypeThrows() + { + var result = Record.Exception(() => Assert.IsType(typeof(InvalidCastException), new InvalidOperationException())); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is not the exact type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidOperationException)", + result.Message + ); + } + +#if NETFRAMEWORK + + [Fact] + public async Task UnmatchedTypesWithIdenticalNamesShowAssemblies() + { + var dynamicAssembly = await CSharpDynamicAssembly.Create("namespace System.Xml { public class XmlException: Exception { } }"); + var assembly = Assembly.LoadFile(dynamicAssembly.FileName); + var dynamicXmlExceptionType = assembly.GetType("System.Xml.XmlException"); + Assert.NotNull(dynamicXmlExceptionType); + var ex = Activator.CreateInstance(dynamicXmlExceptionType); + + var result = Record.Exception(() => Assert.IsType(typeof(XmlException), ex)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is not the exact type" + Environment.NewLine + + "Expected: typeof(System.Xml.XmlException) (from " + typeof(XmlException).Assembly.FullName + ")" + Environment.NewLine + + "Actual: typeof(System.Xml.XmlException) (from " + assembly.FullName + ")", + result.Message + ); + } + +#endif + + [Fact] + public void NullObjectThrows() + { + var result = Record.Exception(() => Assert.IsType(typeof(object), null)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is null" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: null", + result.Message + ); + } + } + + public class IsType_NonGeneric_InexactMatch + { + [Fact] + public void NullObject() + { + var result = Record.Exception(() => Assert.IsType(typeof(object), null, exactMatch: false)); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is null" + Environment.NewLine + + "Expected: typeof(object)" + Environment.NewLine + + "Actual: null", + result.Message + ); + } + + [Fact] + public void SameType() + { + var ex = new InvalidCastException(); + + Assert.IsType(typeof(InvalidCastException), ex, exactMatch: false); + } + + [Fact] + public void BaseType() + { + var ex = new InvalidCastException(); + + Assert.IsType(typeof(Exception), ex, exactMatch: false); + } + + [Fact] + public void Interface() + { + var ex = new DisposableClass(); + + Assert.IsType(typeof(IDisposable), ex, exactMatch: false); + } + + [Fact] + public void ReturnsCastObject() + { + var ex = new InvalidCastException(); + + var result = Assert.IsType(ex, exactMatch: false); + + Assert.Same(ex, result); + } + + [Fact] + public void IncompatibleType() + { + var result = + Record.Exception( + () => Assert.IsType(typeof(InvalidCastException), new InvalidOperationException(), exactMatch: false) + ); + + Assert.IsType(result); + Assert.Equal( + "Assert.IsType() Failure: Value is an incompatible type" + Environment.NewLine + + "Expected: typeof(System.InvalidCastException)" + Environment.NewLine + + "Actual: typeof(System.InvalidOperationException)", + result.Message + ); + } + } + +#pragma warning restore xUnit2007 // Do not use typeof expression to check the type + + class DisposableClass : IDisposable + { + public void Dispose() + { } + } + +#if NETFRAMEWORK + + class CSharpDynamicAssembly : CSharpAcceptanceTestAssembly + { + public CSharpDynamicAssembly() : + base(Path.GetTempPath()) + { } + + protected override IEnumerable GetStandardReferences() => + []; + + public static async Task Create(string code) + { + var assembly = new CSharpDynamicAssembly(); + await assembly.Compile([code]); + return assembly; + } + } + +#endif +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/CulturedFactDefaultAttribute.cs b/src/Microsoft.DotNet.XUnitAssert/tests/CulturedFactDefaultAttribute.cs new file mode 100644 index 00000000000..87a4dfb5792 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/CulturedFactDefaultAttribute.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +namespace Xunit; + +public class CulturedFactDefaultAttribute( + [CallerFilePath] string? sourceFilePath = null, + [CallerLineNumber] int sourceLineNumber = -1) : + CulturedFactAttribute(["en-US", "fr-FR"], sourceFilePath, sourceLineNumber) +{ } diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/CulturedTheoryDefaultAttribute.cs b/src/Microsoft.DotNet.XUnitAssert/tests/CulturedTheoryDefaultAttribute.cs new file mode 100644 index 00000000000..5e09df6bf8c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/CulturedTheoryDefaultAttribute.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +namespace Xunit; + +public class CulturedTheoryDefaultAttribute( + [CallerFilePath] string? sourceFilePath = null, + [CallerLineNumber] int sourceLineNumber = -1) : + CulturedTheoryAttribute(["en-US", "fr-FR"], sourceFilePath, sourceLineNumber) +{ } diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/EnumerableExtensions.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/EnumerableExtensions.cs new file mode 100644 index 00000000000..b48735535cc --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/EnumerableExtensions.cs @@ -0,0 +1,12 @@ +public static class EnumerableExtensions +{ +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + + public async static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable data) + { + foreach (var dataItem in data) + yield return dataItem; + } + +#pragma warning restore CS1998 +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/MemoryExtensions.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/MemoryExtensions.cs new file mode 100644 index 00000000000..617c812135c --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/MemoryExtensions.cs @@ -0,0 +1,8 @@ +internal static class MemoryExtensions +{ + public static Memory Memoryify(this T[]? values) => + new(values); + + public static Memory Memoryify(this string? value) => + new((value ?? string.Empty).ToCharArray()); +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/SetExtensions.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/SetExtensions.cs new file mode 100644 index 00000000000..51c13647f84 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/SetExtensions.cs @@ -0,0 +1,7 @@ +internal static class SetExtensions +{ + public static SortedSet ToSortedSet( + this ISet set, + IComparer? comparer = null) => + new(set, comparer); +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/SpanExtensions.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/SpanExtensions.cs new file mode 100644 index 00000000000..662a52a6864 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Extensions/SpanExtensions.cs @@ -0,0 +1,8 @@ +internal static class SpanExtensions +{ + public static Span Spanify(this T[]? values) => + new(values); + + public static Span Spanify(this string? value) => + new((value ?? string.Empty).ToCharArray()); +} diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/GlobalUsings.cs b/src/Microsoft.DotNet.XUnitAssert/tests/GlobalUsings.cs new file mode 100644 index 00000000000..9e69ce73274 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using System.Diagnostics.CodeAnalysis; +global using Xunit.Internal; diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Microsoft.DotNet.XUnitAssert.Tests.csproj b/src/Microsoft.DotNet.XUnitAssert/tests/Microsoft.DotNet.XUnitAssert.Tests.csproj new file mode 100644 index 00000000000..bd726a272e7 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Microsoft.DotNet.XUnitAssert.Tests.csproj @@ -0,0 +1,29 @@ + + + + $(BundledNETCoreAppTargetFramework) + Exe + enable + enable + true + + $(NoWarn);IDE0073 + $(DefineConstants);XUNIT_NULLABLE;XUNIT_SPAN;XUNIT_IMMUTABLE_COLLECTIONS;XUNIT_AOT + true + + + true + + true + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.XUnitAssert/tests/Utility/ReadOnlySet.cs b/src/Microsoft.DotNet.XUnitAssert/tests/Utility/ReadOnlySet.cs new file mode 100644 index 00000000000..a6987d3fdf1 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitAssert/tests/Utility/ReadOnlySet.cs @@ -0,0 +1,44 @@ +#if NET8_0_OR_GREATER + +using System.Collections; +using System.Collections.Generic; + +public class ReadOnlySet( + IEqualityComparer comparer, + params T[] items) : + IReadOnlySet +{ + readonly HashSet hashSet = new(items, comparer); + + public int Count => + hashSet.Count; + + public bool Contains(T item) => + hashSet.Contains(item); + + public IEnumerator GetEnumerator() => + hashSet.GetEnumerator(); + + public bool IsProperSubsetOf(IEnumerable other) => + hashSet.IsProperSubsetOf(other); + + public bool IsProperSupersetOf(IEnumerable other) => + hashSet.IsProperSupersetOf(other); + + public bool IsSubsetOf(IEnumerable other) => + hashSet.IsSubsetOf(other); + + public bool IsSupersetOf(IEnumerable other) => + hashSet.IsSupersetOf(other); + + public bool Overlaps(IEnumerable other) => + hashSet.Overlaps(other); + + public bool SetEquals(IEnumerable other) => + hashSet.SetEquals(other); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); +} + +#endif diff --git a/tests/UnitTests.proj b/tests/UnitTests.proj index fb5975fcb37..75d73434482 100644 --- a/tests/UnitTests.proj +++ b/tests/UnitTests.proj @@ -49,10 +49,12 @@ + +