From 5c4c622c073385c3731a5f5bc366f69f70d94f4d Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 25 Mar 2023 19:04:57 -0700 Subject: [PATCH 1/5] Delete directory to reset --- src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj diff --git a/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj b/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj deleted file mode 100644 index e433302f4e630..0000000000000 --- a/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - Library - net7.0 - enable - - - \ No newline at end of file From 5c8819064db31f70cf1b2deba4c2b106cbc0c805 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 25 Mar 2023 19:05:00 -0700 Subject: [PATCH 2/5] Squashed 'src/tests/Common/xunit/assert.xunit/' content from commit adf018d77ea git-subtree-dir: src/tests/Common/xunit/assert.xunit git-subtree-split: adf018d77ea89003b0c57b9be1ba88d81ef0937e --- .editorconfig | 219 +++++ .gitattributes | 6 + Assert.cs | 46 + BooleanAsserts.cs | 146 ++++ CollectionAsserts.cs | 631 ++++++++++++++ Comparers.cs | 31 + DictionaryAsserts.cs | 235 +++++ EqualityAsserts.cs | 467 ++++++++++ EquivalenceAsserts.cs | 44 + EventAsserts.cs | 202 +++++ ExceptionAsserts.cs | 380 +++++++++ FailAsserts.cs | 34 + Guards.cs | 33 + IdentityAsserts.cs | 54 ++ LICENSE.txt | 14 + MemoryAsserts.cs | 712 ++++++++++++++++ MultipleAsserts.cs | 49 ++ NullAsserts.cs | 50 ++ PropertyAsserts.cs | 100 +++ README.md | 104 +++ RangeAsserts.cs | 90 ++ Record.cs | 173 ++++ Sdk/ArgumentFormatter.cs | 484 +++++++++++ Sdk/AssertComparer.cs | 52 ++ Sdk/AssertEqualityComparer.cs | 431 ++++++++++ Sdk/AssertEqualityComparerAdapter.cs | 49 ++ Sdk/AssertHelper.cs | 234 +++++ Sdk/DynamicSkipToken.cs | 18 + Sdk/Exceptions/AllException.cs | 70 ++ .../AssertActualExpectedException.cs | 173 ++++ .../AssertCollectionCountException.cs | 28 + Sdk/Exceptions/CollectionException.cs | 131 +++ Sdk/Exceptions/ContainsDuplicateException.cs | 59 ++ Sdk/Exceptions/ContainsException.cs | 33 + Sdk/Exceptions/DoesNotContainException.cs | 33 + Sdk/Exceptions/DoesNotMatchException.cs | 35 + Sdk/Exceptions/EmptyException.cs | 27 + Sdk/Exceptions/EndsWithException.cs | 67 ++ Sdk/Exceptions/EqualException.cs | 316 +++++++ Sdk/Exceptions/EquivalentException.cs | 162 ++++ Sdk/Exceptions/FailException.cs | 25 + Sdk/Exceptions/FalseException.cs | 32 + Sdk/Exceptions/InRangeException.cs | 74 ++ Sdk/Exceptions/IsAssignableFromException.cs | 34 + Sdk/Exceptions/IsNotTypeException.cs | 34 + Sdk/Exceptions/IsTypeException.cs | 33 + Sdk/Exceptions/MatchesException.cs | 35 + Sdk/Exceptions/MultipleException.cs | 46 + Sdk/Exceptions/NotEmptyException.cs | 24 + Sdk/Exceptions/NotEqualException.cs | 31 + Sdk/Exceptions/NotInRangeException.cs | 76 ++ Sdk/Exceptions/NotNullException.cs | 24 + Sdk/Exceptions/NotSameException.cs | 24 + Sdk/Exceptions/NullException.cs | 25 + .../ParameterCountMismatchException.cs | 20 + Sdk/Exceptions/ProperSubsetException.cs | 36 + Sdk/Exceptions/ProperSupersetException.cs | 30 + Sdk/Exceptions/PropertyChangedException.cs | 26 + Sdk/Exceptions/RaisesException.cs | 93 ++ Sdk/Exceptions/SameException.cs | 30 + Sdk/Exceptions/SingleException.cs | 44 + Sdk/Exceptions/SkipException.cs | 26 + Sdk/Exceptions/StartsWithException.cs | 48 ++ Sdk/Exceptions/SubsetException.cs | 30 + Sdk/Exceptions/SupersetException.cs | 30 + Sdk/Exceptions/ThrowsException.cs | 78 ++ Sdk/Exceptions/TrueException.cs | 32 + Sdk/Exceptions/XunitException.cs | 119 +++ Sdk/IAssertionException.cs | 13 + SetAsserts.cs | 231 +++++ SkipAsserts.cs | 79 ++ SpanAsserts.cs | 806 ++++++++++++++++++ StringAsserts.cs | 405 +++++++++ TypeAsserts.cs | 144 ++++ 74 files changed, 9059 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 Assert.cs create mode 100644 BooleanAsserts.cs create mode 100644 CollectionAsserts.cs create mode 100644 Comparers.cs create mode 100644 DictionaryAsserts.cs create mode 100644 EqualityAsserts.cs create mode 100644 EquivalenceAsserts.cs create mode 100644 EventAsserts.cs create mode 100644 ExceptionAsserts.cs create mode 100644 FailAsserts.cs create mode 100644 Guards.cs create mode 100644 IdentityAsserts.cs create mode 100644 LICENSE.txt create mode 100644 MemoryAsserts.cs create mode 100644 MultipleAsserts.cs create mode 100644 NullAsserts.cs create mode 100644 PropertyAsserts.cs create mode 100644 README.md create mode 100644 RangeAsserts.cs create mode 100644 Record.cs create mode 100644 Sdk/ArgumentFormatter.cs create mode 100644 Sdk/AssertComparer.cs create mode 100644 Sdk/AssertEqualityComparer.cs create mode 100644 Sdk/AssertEqualityComparerAdapter.cs create mode 100644 Sdk/AssertHelper.cs create mode 100644 Sdk/DynamicSkipToken.cs create mode 100644 Sdk/Exceptions/AllException.cs create mode 100644 Sdk/Exceptions/AssertActualExpectedException.cs create mode 100644 Sdk/Exceptions/AssertCollectionCountException.cs create mode 100644 Sdk/Exceptions/CollectionException.cs create mode 100644 Sdk/Exceptions/ContainsDuplicateException.cs create mode 100644 Sdk/Exceptions/ContainsException.cs create mode 100644 Sdk/Exceptions/DoesNotContainException.cs create mode 100644 Sdk/Exceptions/DoesNotMatchException.cs create mode 100644 Sdk/Exceptions/EmptyException.cs create mode 100644 Sdk/Exceptions/EndsWithException.cs create mode 100644 Sdk/Exceptions/EqualException.cs create mode 100644 Sdk/Exceptions/EquivalentException.cs create mode 100644 Sdk/Exceptions/FailException.cs create mode 100644 Sdk/Exceptions/FalseException.cs create mode 100644 Sdk/Exceptions/InRangeException.cs create mode 100644 Sdk/Exceptions/IsAssignableFromException.cs create mode 100644 Sdk/Exceptions/IsNotTypeException.cs create mode 100644 Sdk/Exceptions/IsTypeException.cs create mode 100644 Sdk/Exceptions/MatchesException.cs create mode 100644 Sdk/Exceptions/MultipleException.cs create mode 100644 Sdk/Exceptions/NotEmptyException.cs create mode 100644 Sdk/Exceptions/NotEqualException.cs create mode 100644 Sdk/Exceptions/NotInRangeException.cs create mode 100644 Sdk/Exceptions/NotNullException.cs create mode 100644 Sdk/Exceptions/NotSameException.cs create mode 100644 Sdk/Exceptions/NullException.cs create mode 100644 Sdk/Exceptions/ParameterCountMismatchException.cs create mode 100644 Sdk/Exceptions/ProperSubsetException.cs create mode 100644 Sdk/Exceptions/ProperSupersetException.cs create mode 100644 Sdk/Exceptions/PropertyChangedException.cs create mode 100644 Sdk/Exceptions/RaisesException.cs create mode 100644 Sdk/Exceptions/SameException.cs create mode 100644 Sdk/Exceptions/SingleException.cs create mode 100644 Sdk/Exceptions/SkipException.cs create mode 100644 Sdk/Exceptions/StartsWithException.cs create mode 100644 Sdk/Exceptions/SubsetException.cs create mode 100644 Sdk/Exceptions/SupersetException.cs create mode 100644 Sdk/Exceptions/ThrowsException.cs create mode 100644 Sdk/Exceptions/TrueException.cs create mode 100644 Sdk/Exceptions/XunitException.cs create mode 100644 Sdk/IAssertionException.cs create mode 100644 SetAsserts.cs create mode 100644 SkipAsserts.cs create mode 100644 SpanAsserts.cs create mode 100644 StringAsserts.cs create mode 100644 TypeAsserts.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000..7d00532d7f076 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,219 @@ +# top-most EditorConfig file +root = true + +[*] +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +# 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 + +# this. and Me. preferences +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_property = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_event = 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_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 = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +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_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,volatile,async + +# Code-block preferences +csharp_prefer_braces = false +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = block_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 + +#### Roslyn diagnostics #### + +dotnet_diagnostic.IDE1006.severity = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000..f062fc0782817 --- /dev/null +++ b/.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/Assert.cs b/Assert.cs new file mode 100644 index 0000000000000..f367b66d2f011 --- /dev/null +++ b/Assert.cs @@ -0,0 +1,46 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +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 + { + /// + /// 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 new static 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 new static bool ReferenceEquals( + object a, + object b) + { + throw new InvalidOperationException("Assert.ReferenceEquals should not be used"); + } + } +} diff --git a/BooleanAsserts.cs b/BooleanAsserts.cs new file mode 100644 index 0000000000000..380c75f11ce62 --- /dev/null +++ b/BooleanAsserts.cs @@ -0,0 +1,146 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 new FalseException(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 new TrueException(userMessage, condition); + } + } +} diff --git a/CollectionAsserts.cs b/CollectionAsserts.cs new file mode 100644 index 0000000000000..66c208d4aa8e5 --- /dev/null +++ b/CollectionAsserts.cs @@ -0,0 +1,631 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Xunit.Sdk; + +#if XUNIT_VALUETASK +using System.Threading.Tasks; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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)); + } + + /// + /// 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); + +#if XUNIT_NULLABLE + var errors = new Stack>(); +#else + var errors = new Stack>(); +#endif + var idx = 0; + + foreach (var item in collection) + { + try + { + action(item, idx); + } + catch (Exception ex) + { +#if XUNIT_NULLABLE + errors.Push(new Tuple(idx, item, ex)); +#else + errors.Push(new Tuple(idx, item, ex)); +#endif + } + + ++idx; + } + + if (errors.Count > 0) + throw new AllException(idx, errors.ToArray()); + } + +#if XUNIT_VALUETASK + /// + /// 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 ValueTask AllAsync( + IEnumerable collection, + Func action) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + + await AllAsync(collection, async (item, index) => await action(item)); + } + + /// + /// 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 ValueTask AllAsync( + IEnumerable collection, + Func action) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(action), action); + +#if XUNIT_NULLABLE + var errors = new Stack>(); +#else + var errors = new Stack>(); +#endif + var idx = 0; + + foreach (var item in collection) + { + try + { + await action(item, idx); + } + catch (Exception ex) + { +#if XUNIT_NULLABLE + errors.Push(new Tuple(idx, item, ex)); +#else + errors.Push(new Tuple(idx, item, ex)); +#endif + } + + ++idx; + } + + if (errors.Count > 0) + throw new AllException(idx, errors.ToArray()); + } +#endif + + /// + /// 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); + + var elements = collection.ToArray(); + var expectedCount = elementInspectors.Length; + var actualCount = elements.Length; + + if (expectedCount != actualCount) + throw new CollectionException(collection, expectedCount, actualCount); + + for (var idx = 0; idx < actualCount; idx++) + { + try + { + elementInspectors[idx](elements[idx]); + } + catch (Exception ex) + { + throw new CollectionException(collection, expectedCount, actualCount, idx, ex); + } + } + } + +#if XUNIT_VALUETASK + /// + /// 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 ValueTask CollectionAsync( + IEnumerable collection, + params Func[] elementInspectors) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(elementInspectors), elementInspectors); + + var elements = collection.ToArray(); + var expectedCount = elementInspectors.Length; + var actualCount = elements.Length; + + if (expectedCount != actualCount) + throw new CollectionException(collection, expectedCount, actualCount); + + for (var idx = 0; idx < actualCount; idx++) + try + { + await elementInspectors[idx](elements[idx]); + } + catch (Exception ex) + { + throw new CollectionException(collection, expectedCount, actualCount, idx, ex); + } + } +#endif + + /// + /// 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); + + // If an equality comparer is not explicitly provided, call into ICollection.Contains or + // IReadOnlyCollection.Contains which may use the collection's equality comparer for types + // like HashSet and Dictionary. HashSet and Dictionary are normally handled explicitly, but + // the developer may end up in the IEnumerable<> override because the variable is not an explicit + // enough type. + var readWriteCollection = collection as ICollection; + if (readWriteCollection != null) + { + if (readWriteCollection.Contains(expected)) + return; + } + else + { + var readOnlyCollection = collection as IReadOnlyCollection; + if (readOnlyCollection != null && readOnlyCollection.Contains(expected)) + return; + } + + 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); + + if (collection.Contains(expected, comparer)) + return; + + throw new ContainsException(expected, collection); + } + + /// + /// 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); + + foreach (var item in collection) + if (filter(item)) + return; + + throw new ContainsException("(filter expression)", collection); + } + + /// + /// 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 container 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 container more than once + public static void Distinct( + IEnumerable collection, + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(comparer), comparer); + + var set = new HashSet(comparer); + + foreach (var x in collection) + if (!set.Add(x)) + throw new ContainsDuplicateException(x, collection); + } + + /// + /// 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 container + public static void DoesNotContain( + T expected, + IEnumerable collection) + { + GuardArgumentNotNull(nameof(collection), collection); + + // If an equality comparer is not explicitly provided, call into ICollection.Contains or + // IReadOnlyCollection.Contains which may use the collection's equality comparer for types + // like HashSet and Dictionary. HashSet and Dictionary are normally handled explicitly, but + // the developer may end up in the IEnumerable<> override because the variable is not an explicit + // enough type. + var readWriteCollection = collection as ICollection; + if (readWriteCollection != null) + { + if (readWriteCollection.Contains(expected)) + throw new DoesNotContainException(expected, collection); + } + else + { + var readOnlyCollection = collection as IReadOnlyCollection; + if (readOnlyCollection != null && readOnlyCollection.Contains(expected)) + throw new DoesNotContainException(expected, collection); + } + + 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 container + public static void DoesNotContain( + T expected, + IEnumerable collection, + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(comparer), comparer); + + if (!collection.Contains(expected, comparer)) + return; + + throw new DoesNotContainException(expected, collection); + } + + /// + /// 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 container + public static void DoesNotContain( + IEnumerable collection, + Predicate filter) + { + GuardArgumentNotNull(nameof(collection), collection); + GuardArgumentNotNull(nameof(filter), filter); + + foreach (var item in collection) + if (filter(item)) + throw new DoesNotContainException("(filter expression)", collection); + } + + /// + /// 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); + + var enumerator = collection.GetEnumerator(); + try + { + if (enumerator.MoveNext()) + throw new EmptyException(collection); + } + finally + { + (enumerator as IDisposable)?.Dispose(); + } + } + + /// + /// 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 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 new NotEmptyException(); + } + 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 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, "(filter expression)"); + } + + static T GetSingleResult( + IEnumerable collection, +#if XUNIT_NULLABLE + Predicate? predicate, + string? expectedArgument) +#else + Predicate predicate, + string expectedArgument) +#endif + { + var count = 0; + var result = default(T); + + foreach (var item in collection) + if (predicate == null || predicate(item)) + if (++count == 1) + result = item; + + switch (count) + { + case 0: + throw SingleException.Empty(expectedArgument); + case 1: +#if XUNIT_NULLABLE + return result!; +#else + return result; +#endif + default: + throw SingleException.MoreThanOne(count, expectedArgument); + } + } + } +} diff --git a/Comparers.cs b/Comparers.cs new file mode 100644 index 0000000000000..cfa736a1a7bdf --- /dev/null +++ b/Comparers.cs @@ -0,0 +1,31 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + static IComparer GetComparer() + where T : IComparable => + new AssertComparer(); + +#if XUNIT_NULLABLE + static IEqualityComparer GetEqualityComparer(IEqualityComparer? innerComparer = null) => + new AssertEqualityComparer(innerComparer); +#else + static IEqualityComparer GetEqualityComparer(IEqualityComparer innerComparer = null) => + new AssertEqualityComparer(innerComparer); +#endif + } +} diff --git a/DictionaryAsserts.cs b/DictionaryAsserts.cs new file mode 100644 index 0000000000000..9b364fae75eb7 --- /dev/null +++ b/DictionaryAsserts.cs @@ -0,0 +1,235 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Xunit.Sdk; + +#if XUNIT_IMMUTABLE_COLLECTIONS +using System.Collections.Immutable; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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); + + var value = default(TValue); + if (!collection.TryGetValue(expected, out value)) + throw new ContainsException(expected, 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); + + var value = default(TValue); + if (!collection.TryGetValue(expected, out value)) + throw new ContainsException(expected, 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, + Dictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + { + return 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 + { + return Contains(expected, (IReadOnlyDictionary)collection); + } + +#if XUNIT_IMMUTABLE_COLLECTIONS + /// + /// 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 + { + return Contains(expected, (IReadOnlyDictionary)collection); + } +#endif + + /// + /// 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 new DoesNotContainException(expected, 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 new DoesNotContainException(expected, 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, + Dictionary collection) +#if XUNIT_NULLABLE + where TKey : notnull +#endif + { + DoesNotContain(expected, (IDictionary)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); + } + +#if XUNIT_IMMUTABLE_COLLECTIONS + /// + /// 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); + } +#endif + } +} diff --git a/EqualityAsserts.cs b/EqualityAsserts.cs new file mode 100644 index 0000000000000..8e035d57e3cb0 --- /dev/null +++ b/EqualityAsserts.cs @@ -0,0 +1,467 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { +#if XUNIT_SPAN + /// + /// Verifies that two arrays of un-managed type T are equal, using Span<T>.SequenceEqual. + /// + /// The type of items whose arrays are to be compared + /// The expected value + /// The value to be compared against + /// Thrown when the arrays are not equal + /// + /// If Span<T>.SequenceEqual fails, a call to Assert.Equal(object, object) is made, + /// to provide a more meaningful error message. + /// + public static void Equal( +#if XUNIT_NULLABLE + [AllowNull] T[] expected, + [AllowNull] T[] actual) + where T : unmanaged, IEquatable +#else + T[] expected, + T[] actual) + where T : IEquatable +#endif + { + if (expected == null && actual == null) + return; + + // Call into Equal so we get proper formatting of the sequence + if (expected == null || actual == null || !expected.AsSpan().SequenceEqual(actual)) + Equal(expected, actual); + } +#endif + + /// + /// 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 + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + [AllowNull] T expected, + [AllowNull] T actual) => +#else + T expected, + T actual) => +#endif + Equal(expected, actual, GetEqualityComparer()); + + /// + /// Verifies that two objects are equal, 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 + [AllowNull] T expected, + [AllowNull] T actual, +#else + T expected, + T actual, +#endif + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(comparer), comparer); + + var expectedAsIEnum = expected as IEnumerable; + var actualAsIEnum = actual as IEnumerable; + var aec = comparer as AssertEqualityComparer; + + // if we got an AssertEqualityComparer we can invoke it to get the mismatched index. + if (aec != null) + { + int? mismatchedIndex; + + if (!aec.Equals(expected, actual, out mismatchedIndex)) + { + if (mismatchedIndex.HasValue) + throw EqualException.FromEnumerable(expectedAsIEnum, actualAsIEnum, mismatchedIndex.Value); + else + throw new EqualException(expected, actual); + } + } + else if (!comparer.Equals(expected, actual)) + throw new EqualException(expected, actual); + } + + /// + /// 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) + /// Thrown when the values are not equal + 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 new EqualException($"{expectedRounded} (rounded from {expected})", $"{actualRounded} (rounded from {actual})"); + } + + /// + /// 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 new EqualException($"{expectedRounded} (rounded from {expected})", $"{actualRounded} (rounded from {actual})"); + } + + /// + /// 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 + /// Thrown when supplied tolerance is invalid" + /// Thrown when the values are not equal + 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 new EqualException($"{expected:G17}", $"{actual:G17}"); + } + + /// + /// 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) + /// Thrown when the values are not equal + public static void Equal( + float expected, + float actual, + int precision) => + Equal((double)expected, (double)actual, precision); + + /// + /// 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) => + Equal((double)expected, (double)actual, precision, rounding); + + /// + /// 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 + /// Thrown when supplied tolerance is invalid" + /// Thrown when the values are not equal + 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 new EqualException($"{expected:G9}", $"{actual:G9}"); + } + + /// + /// 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) + /// Thrown when the values are not equal + 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 new EqualException($"{expectedRounded} (rounded from {expected})", $"{actualRounded} (rounded from {actual})"); + } + + /// + /// Verifies that two values are equal. + /// + /// The expected value + /// The value to be compared against + /// Thrown when the values are not equal + 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 + /// Thrown when the values are not within the given precision + public static void Equal( + DateTime expected, + DateTime actual, + TimeSpan precision) + { + var difference = (expected - actual).Duration(); + + if (difference > precision) + { + var actualValue = + precision == TimeSpan.Zero + ? actual.ToString() + : $"{actual} (difference {difference} is larger than {precision})"; + + throw new EqualException(expected.ToString(), actualValue); + } + } + + /// + /// Verifies that two values are equal. + /// + /// The expected value + /// The value to be compared against + /// Thrown when the values are not equal + 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 + /// Thrown when the values are not within the given precision + public static void Equal( + DateTimeOffset expected, + DateTimeOffset actual, + TimeSpan precision) + { + var difference = (expected - actual).Duration(); + + if (difference > precision) + { + var actualValue = + precision == TimeSpan.Zero + ? actual.ToString() + : $"{actual} (difference {difference} is larger than {precision})"; + + throw new EqualException(expected.ToString(), actualValue); + } + } + + /// + /// 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 + /// Thrown when the objects are not equal + public static void StrictEqual( +#if XUNIT_NULLABLE + [AllowNull] T expected, + [AllowNull] T actual) => + Equal(expected, actual, EqualityComparer.Default); +#else + T expected, + T actual) => + Equal(expected, actual, EqualityComparer.Default); +#endif + +#if XUNIT_SPAN + /// + /// 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 + /// Thrown when the arrays are equal + public static void NotEqual( +#if XUNIT_NULLABLE + [AllowNull] T[] expected, + [AllowNull] 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); + } +#endif + + /// + /// 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 + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + [AllowNull] T expected, + [AllowNull] T actual) => +#else + T expected, + T actual) => +#endif + NotEqual(expected, actual, GetEqualityComparer()); + + /// + /// 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 + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + [AllowNull] T expected, + [AllowNull] T actual, +#else + T expected, + T actual, +#endif + IEqualityComparer comparer) + { + GuardArgumentNotNull(nameof(comparer), comparer); + + if (comparer.Equals(expected, actual)) + throw new NotEqualException(ArgumentFormatter.Format(expected), ArgumentFormatter.Format(actual)); + } + + /// + /// 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) + /// Thrown when the values are equal + 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 new NotEqualException($"{expectedRounded} (rounded from {expected})", $"{actualRounded} (rounded from {actual})"); + } + + /// + /// 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) + /// Thrown when the values are equal + 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 new NotEqualException($"{expectedRounded} (rounded from {expected})", $"{actualRounded} (rounded from {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 + /// Thrown when the objects are equal + public static void NotStrictEqual( +#if XUNIT_NULLABLE + [AllowNull] T expected, + [AllowNull] T actual) => + NotEqual(expected, actual, EqualityComparer.Default); +#else + T expected, + T actual) => + NotEqual(expected, actual, EqualityComparer.Default); +#endif + } +} diff --git a/EquivalenceAsserts.cs b/EquivalenceAsserts.cs new file mode 100644 index 0000000000000..28db8e7ab6932 --- /dev/null +++ b/EquivalenceAsserts.cs @@ -0,0 +1,44 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using Xunit.Internal; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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; + } + } +} diff --git a/EventAsserts.cs b/EventAsserts.cs new file mode 100644 index 0000000000000..b6a75fd23af22 --- /dev/null +++ b/EventAsserts.cs @@ -0,0 +1,202 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + /// + /// Verifies that a 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); + + if (raisedEvent == null) + throw new RaisesException(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw new RaisesException(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + /// + /// 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) + { + var raisedEvent = RaisesInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw new RaisesException(typeof(T)); + + return raisedEvent; + } + + /// + /// Verifies that a 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); + + if (raisedEvent == null) + throw new RaisesException(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw new RaisesException(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + /// + /// 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) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw new RaisesException(typeof(T)); + + return raisedEvent; + } + +#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); + +#if XUNIT_NULLABLE + RaisedEvent? raisedEvent = null; + void handler(object? s, T args) => raisedEvent = new RaisedEvent(s, args); +#else + RaisedEvent raisedEvent = null; + EventHandler handler = (object s, T args) => raisedEvent = new RaisedEvent(s, args); +#endif + attach(handler); + 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); + +#if XUNIT_NULLABLE + RaisedEvent? raisedEvent = null; + void handler(object? s, T args) => raisedEvent = new RaisedEvent(s, args); +#else + RaisedEvent raisedEvent = null; + EventHandler handler = (object s, T args) => raisedEvent = new RaisedEvent(s, args); +#endif + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + + /// + /// Represents a raised event after the fact. + /// + /// The type of the event arguments. + public class RaisedEvent + { + /// + /// The sender of the event. + /// +#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 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/ExceptionAsserts.cs b/ExceptionAsserts.cs new file mode 100644 index 0000000000000..b45312fdd114e --- /dev/null +++ b/ExceptionAsserts.cs @@ -0,0 +1,380 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.ComponentModel; +using System.Reflection; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static T Throws(Action testCode) + where T : Exception => + (T)Throws(typeof(T), RecordException(testCode)); + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown +#if XUNIT_NULLABLE + public static T Throws(Func testCode) +#else + public static T Throws(Func testCode) +#endif + where T : Exception => + (T)Throws(typeof(T), RecordException(testCode)); + + /// + [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 NotImplementedException(); + } + +#if XUNIT_VALUETASK + /// + [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 NotImplementedException(); + } +#endif + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async Task ThrowsAsync(Func testCode) + where T : Exception => + (T)Throws(typeof(T), await RecordExceptionAsync(testCode)); + +#if XUNIT_VALUETASK + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async ValueTask ThrowsAsync(Func testCode) + where T : Exception => + (T)Throws(typeof(T), await RecordExceptionAsync(testCode)); +#endif + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static T ThrowsAny(Action testCode) + where T : Exception => + (T)ThrowsAny(typeof(T), RecordException(testCode)); + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown +#if XUNIT_NULLABLE + public static T ThrowsAny(Func testCode) +#else + public static T ThrowsAny(Func testCode) +#endif + where T : Exception => + (T)ThrowsAny(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 task to be tested + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async Task ThrowsAnyAsync(Func testCode) + where T : Exception => + (T)ThrowsAny(typeof(T), await RecordExceptionAsync(testCode)); + +#if XUNIT_VALUETASK + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async ValueTask ThrowsAnyAsync(Func testCode) + where T : Exception => + (T)ThrowsAny(typeof(T), await RecordExceptionAsync(testCode)); +#endif + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static Exception Throws( + Type exceptionType, + Action testCode) => + Throws(exceptionType, RecordException(testCode)); + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static Exception Throws( + Type exceptionType, +#if XUNIT_NULLABLE + Func testCode) => +#else + Func testCode) => +#endif + Throws(exceptionType, RecordException(testCode)); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] + public static Exception Throws( + string paramName, + Func testCode) + { + throw new NotImplementedException(); + } + +#if XUNIT_VALUETASK + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] + public static Exception Throws( + string paramName, + Func testCode) + { + throw new NotImplementedException(); + } +#endif + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async Task ThrowsAsync( + Type exceptionType, + Func testCode) => + Throws(exceptionType, await RecordExceptionAsync(testCode)); + +#if XUNIT_VALUETASK + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async ValueTask ThrowsAsync( + Type exceptionType, + Func testCode) => + Throws(exceptionType, await RecordExceptionAsync(testCode)); +#endif + + static Exception Throws( + Type exceptionType, +#if XUNIT_NULLABLE + Exception? exception) +#else + Exception exception) +#endif + { + GuardArgumentNotNull(nameof(exceptionType), exceptionType); + + if (exception == null) + throw new ThrowsException(exceptionType); + + if (!exceptionType.Equals(exception.GetType())) + throw new ThrowsException(exceptionType, exception); + + return exception; + } + + static Exception ThrowsAny( + Type exceptionType, +#if XUNIT_NULLABLE + Exception? exception) +#else + Exception exception) +#endif + { + GuardArgumentNotNull(nameof(exceptionType), exceptionType); + + if (exception == null) + throw new ThrowsException(exceptionType); + + if (!exceptionType.GetTypeInfo().IsAssignableFrom(exception.GetType().GetTypeInfo())) + throw new ThrowsException(exceptionType, exception); + + return exception; + } + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static T Throws( +#if XUNIT_NULLABLE + string? paramName, +#else + string paramName, +#endif + Action testCode) + where T : ArgumentException + { + var ex = Throws(testCode); + Equal(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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static T Throws( +#if XUNIT_NULLABLE + string? paramName, + Func testCode) +#else + string paramName, + Func testCode) +#endif + where T : ArgumentException + { + var ex = Throws(testCode); + Equal(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 NotImplementedException(); + } + +#if XUNIT_VALUETASK + /// + [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 NotImplementedException(); + } +#endif + + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async Task ThrowsAsync( +#if XUNIT_NULLABLE + string? paramName, +#else + string paramName, +#endif + Func testCode) + where T : ArgumentException + { + var ex = await ThrowsAsync(testCode); + Equal(paramName, ex.ParamName); + return ex; + } + +#if XUNIT_VALUETASK + /// + /// 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 + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async ValueTask ThrowsAsync( +#if XUNIT_NULLABLE + string? paramName, +#else + string paramName, +#endif + Func testCode) + where T : ArgumentException + { + var ex = await ThrowsAsync(testCode); + Equal(paramName, ex.ParamName); + return ex; + } +#endif + } +} diff --git a/FailAsserts.cs b/FailAsserts.cs new file mode 100644 index 0000000000000..c1bc0ee08cd23 --- /dev/null +++ b/FailAsserts.cs @@ -0,0 +1,34 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + /// + /// Indicates that the test should immediately fail. + /// + /// The failure message +#if XUNIT_NULLABLE + [DoesNotReturn] +#endif + public static void Fail(string message) + { + GuardArgumentNotNull(nameof(message), message); + + throw new FailException(message); + } + } +} diff --git a/Guards.cs b/Guards.cs new file mode 100644 index 0000000000000..6457780f39d60 --- /dev/null +++ b/Guards.cs @@ -0,0 +1,33 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + /// + internal static void GuardArgumentNotNull( + string argName, +#if XUNIT_NULLABLE + [NotNull] object? argValue) +#else + object argValue) +#endif + { + if (argValue == null) + throw new ArgumentNullException(argName); + } + } +} diff --git a/IdentityAsserts.cs b/IdentityAsserts.cs new file mode 100644 index 0000000000000..aded28171af39 --- /dev/null +++ b/IdentityAsserts.cs @@ -0,0 +1,54 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 new NotSameException(); + } + + /// + /// 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 new SameException(expected, actual); + } + } +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000000..67f90a6aaac41 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,14 @@ +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. \ No newline at end of file diff --git a/MemoryAsserts.cs b/MemoryAsserts.cs new file mode 100644 index 0000000000000..19eeb954967d7 --- /dev/null +++ b/MemoryAsserts.cs @@ -0,0 +1,712 @@ +#if XUNIT_SPAN + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + // NOTE: there is an implicit conversion operator on Memory to ReadOnlyMemory - however, I have found that the compiler sometimes struggles + // with identifying the proper methods to use, thus I have overloaded quite a few of the assertions in terms of supplying both + // Memory and ReadOnlyMemory based methods + + // NOTE: we could consider StartsWith and EndsWith with both arguments as ReadOnlyMemory, and use the Memory extension methods on Span to check difference + // BUT: the current Exceptions for startswith and endswith are only built for string types, so those would need a change (or new non-string versions created). + + // NOTE: Memory and ReadonlyMemory, even when null, are coerced into empty arrays of the specified type when a value is grabbed. Thus some of the code below + // for null scenarios looks odd, but is safe and correct. + + /// + /// Verifies that a Memory contains a given sub-Memory, using the default comparison type. + /// + /// 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) => + Contains((ReadOnlyMemory)expectedSubMemory, (ReadOnlyMemory)actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory contains a given sub-Memory, using the default comparison type. + /// + /// 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) => + Contains((ReadOnlyMemory)expectedSubMemory, actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory contains a given sub-Memory, using the default comparison type. + /// + /// 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) => + Contains(expectedSubMemory, (ReadOnlyMemory)actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory contains a given sub-Memory, using the default comparison type. + /// + /// 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) => + Contains(expectedSubMemory, actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory contains a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be in the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-Memory is not present inside the Memory + public static void Contains( + Memory expectedSubMemory, + Memory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlyMemory)expectedSubMemory, (ReadOnlyMemory)actualMemory, comparisonType); + + /// + /// Verifies that a Memory contains a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be in the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-Memory is not present inside the Memory + public static void Contains( + Memory expectedSubMemory, + ReadOnlyMemory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlyMemory)expectedSubMemory, actualMemory, comparisonType); + + /// + /// Verifies that a Memory contains a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be in the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-Memory is not present inside the Memory + public static void Contains( + ReadOnlyMemory expectedSubMemory, + Memory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains(expectedSubMemory, (ReadOnlyMemory)actualMemory, comparisonType); + + /// + /// Verifies that a Memory contains a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be in the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-Memory is not present inside the Memory + public static void Contains( + ReadOnlyMemory expectedSubMemory, + ReadOnlyMemory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + GuardArgumentNotNull(nameof(expectedSubMemory), expectedSubMemory); + + Contains(expectedSubMemory.Span, actualMemory.Span, comparisonType); + } + + /// + /// 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 new ContainsException(expectedSubMemory, actualMemory); + } + + /// + /// Verifies that a Memory does not contain a given sub-Memory, using the default comparison type. + /// + /// 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) => + DoesNotContain((ReadOnlyMemory)expectedSubMemory, (ReadOnlyMemory)actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory does not contain a given sub-Memory, using the default comparison type. + /// + /// 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) => + DoesNotContain((ReadOnlyMemory)expectedSubMemory, actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory does not contain a given sub-Memory, using the default comparison type. + /// + /// 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) => + DoesNotContain(expectedSubMemory, (ReadOnlyMemory)actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory does not contain a given sub-Memory, using the default comparison type. + /// + /// 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) => + DoesNotContain(expectedSubMemory, actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory does not contain a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected not to be in the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-Memory is present inside the Memory + public static void DoesNotContain( + Memory expectedSubMemory, + Memory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlyMemory)expectedSubMemory, (ReadOnlyMemory)actualMemory, comparisonType); + + /// + /// Verifies that a Memory does not contain a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected not to be in the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-Memory is present inside the Memory + public static void DoesNotContain( + Memory expectedSubMemory, + ReadOnlyMemory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlyMemory)expectedSubMemory, actualMemory, comparisonType); + + /// + /// Verifies that a Memory does not contain a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected not to be in the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-Memory is present inside the Memory + public static void DoesNotContain( + ReadOnlyMemory expectedSubMemory, + Memory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain(expectedSubMemory, (ReadOnlyMemory)actualMemory, comparisonType); + + /// + /// Verifies that a Memory does not contain a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected not to be in the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-Memory is present inside the Memory + public static void DoesNotContain( + ReadOnlyMemory expectedSubMemory, + ReadOnlyMemory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + GuardArgumentNotNull(nameof(expectedSubMemory), expectedSubMemory); + + DoesNotContain(expectedSubMemory.Span, actualMemory.Span, comparisonType); + } + + /// + /// 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); + + if (actualMemory.Span.IndexOf(expectedSubMemory.Span) > -1) + throw new DoesNotContainException(expectedSubMemory, actualMemory); + } + + /// + /// Verifies that a Memory starts with a given sub-Memory, using the default comparison type. + /// + /// The sub-Memory expected to be at the start of the Memory + /// The Memory to be inspected + /// Thrown when the Memory does not start with the expected subMemory + public static void StartsWith( + Memory expectedStartMemory, + Memory actualMemory) => + StartsWith((ReadOnlyMemory)expectedStartMemory, (ReadOnlyMemory)actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory starts with a given sub-Memory, using the default comparison type. + /// + /// The sub-Memory expected to be at the start of the Memory + /// The Memory to be inspected + /// Thrown when the Memory does not start with the expected subMemory + public static void StartsWith( + Memory expectedStartMemory, + ReadOnlyMemory actualMemory) => + StartsWith((ReadOnlyMemory)expectedStartMemory, actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory starts with a given sub-Memory, using the default comparison type. + /// + /// The sub-Memory expected to be at the start of the Memory + /// The Memory to be inspected + /// Thrown when the Memory does not start with the expected subMemory + public static void StartsWith( + ReadOnlyMemory expectedStartMemory, + Memory actualMemory) => + StartsWith(expectedStartMemory, (ReadOnlyMemory)actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory starts with a given sub-Memory, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-Memory expected to be at the start of the Memory + /// The Memory to be inspected + /// Thrown when the Memory does not start with the expected subMemory + public static void StartsWith( + ReadOnlyMemory expectedStartMemory, + ReadOnlyMemory actualMemory) => + StartsWith(expectedStartMemory, actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory starts with a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be at the start of the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the Memory does not start with the expected subMemory + public static void StartsWith( + Memory expectedStartMemory, + Memory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlyMemory)expectedStartMemory, (ReadOnlyMemory)actualMemory, comparisonType); + + /// + /// Verifies that a Memory starts with a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be at the start of the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the Memory does not start with the expected subMemory + public static void StartsWith( + Memory expectedStartMemory, + ReadOnlyMemory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlyMemory)expectedStartMemory, actualMemory, comparisonType); + + /// + /// Verifies that a Memory starts with a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be at the start of the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the Memory does not start with the expected subMemory + public static void StartsWith( + ReadOnlyMemory expectedStartMemory, + Memory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith(expectedStartMemory, (ReadOnlyMemory)actualMemory, comparisonType); + + /// + /// Verifies that a Memory starts with a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be at the start of the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the Memory does not start with the expected subMemory + public static void StartsWith( + ReadOnlyMemory expectedStartMemory, + ReadOnlyMemory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + GuardArgumentNotNull(nameof(expectedStartMemory), expectedStartMemory); + + StartsWith(expectedStartMemory.Span, actualMemory.Span, comparisonType); + } + + /// + /// Verifies that a Memory ends with a given sub-Memory, using the default comparison type. + /// + /// The sub-Memory expected to be at the end of the Memory + /// The Memory to be inspected + /// Thrown when the Memory does not end with the expected subMemory + public static void EndsWith( + Memory expectedEndMemory, + Memory actualMemory) => + EndsWith((ReadOnlyMemory)expectedEndMemory, (ReadOnlyMemory)actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory ends with a given sub-Memory, using the default comparison type. + /// + /// The sub-Memory expected to be at the end of the Memory + /// The Memory to be inspected + /// Thrown when the Memory does not end with the expected subMemory + public static void EndsWith( + Memory expectedEndMemory, + ReadOnlyMemory actualMemory) => + EndsWith((ReadOnlyMemory)expectedEndMemory, actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory ends with a given sub-Memory, using the default comparison type. + /// + /// The sub-Memory expected to be at the end of the Memory + /// The Memory to be inspected + /// Thrown when the Memory does not end with the expected subMemory + public static void EndsWith( + ReadOnlyMemory expectedEndMemory, + Memory actualMemory) => + EndsWith(expectedEndMemory, (ReadOnlyMemory)actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory ends with a given sub-Memory, using the default comparison type. + /// + /// The sub-Memory expected to be at the end of the Memory + /// The Memory to be inspected + /// Thrown when the Memory does not end with the expected subMemory + public static void EndsWith( + ReadOnlyMemory expectedEndMemory, + ReadOnlyMemory actualMemory) => + EndsWith(expectedEndMemory, actualMemory, StringComparison.CurrentCulture); + + /// + /// Verifies that a Memory ends with a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be at the end of the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the Memory does not end with the expected subMemory + public static void EndsWith( + Memory expectedEndMemory, + Memory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlyMemory)expectedEndMemory, (ReadOnlyMemory)actualMemory, comparisonType); + + /// + /// Verifies that a Memory ends with a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be at the end of the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the Memory does not end with the expected subMemory + public static void EndsWith( + Memory expectedEndMemory, + ReadOnlyMemory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlyMemory)expectedEndMemory, actualMemory, comparisonType); + + /// + /// Verifies that a Memory ends with a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be at the end of the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the Memory does not end with the expected subMemory + public static void EndsWith( + ReadOnlyMemory expectedEndMemory, + Memory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith(expectedEndMemory, (ReadOnlyMemory)actualMemory, comparisonType); + + /// + /// Verifies that a Memory ends with a given sub-Memory, using the given comparison type. + /// + /// The sub-Memory expected to be at the end of the Memory + /// The Memory to be inspected + /// The type of string comparison to perform + /// Thrown when the Memory does not end with the expected subMemory + public static void EndsWith( + ReadOnlyMemory expectedEndMemory, + ReadOnlyMemory actualMemory, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + GuardArgumentNotNull(nameof(expectedEndMemory), expectedEndMemory); + + EndsWith(expectedEndMemory.Span, actualMemory.Span, comparisonType); + } + + /// + /// 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) => + Equal((ReadOnlyMemory)expectedMemory, (ReadOnlyMemory)actualMemory, false, false, false); + + /// + /// 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) => + Equal((ReadOnlyMemory)expectedMemory, actualMemory, false, false, false); + + /// + /// 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) => + Equal(expectedMemory, (ReadOnlyMemory)actualMemory, false, false, false); + + /// + /// 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) => + Equal(expectedMemory, actualMemory, false, false, false); + + /// + /// Verifies that two Memory values are equivalent. + /// + /// The expected Memory value. + /// The actual Memory value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// Thrown when the Memory values are not equivalent. + public static void Equal( + Memory expectedMemory, + Memory actualMemory, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false) => + Equal((ReadOnlyMemory)expectedMemory, (ReadOnlyMemory)actualMemory, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences); + + /// + /// Verifies that two Memory values are equivalent. + /// + /// The expected Memory value. + /// The actual Memory value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// Thrown when the Memory values are not equivalent. + public static void Equal( + Memory expectedMemory, + ReadOnlyMemory actualMemory, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false) => + Equal((ReadOnlyMemory)expectedMemory, actualMemory, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences); + + /// + /// Verifies that two Memory values are equivalent. + /// + /// The expected Memory value. + /// The actual Memory value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// Thrown when the Memory values are not equivalent. + public static void Equal( + ReadOnlyMemory expectedMemory, + Memory actualMemory, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false) => + Equal(expectedMemory, (ReadOnlyMemory)actualMemory, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences); + + /// + /// Verifies that two Memory values are equivalent. + /// + /// The expected Memory value. + /// The actual Memory value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// Thrown when the Memory values are not equivalent. + public static void Equal( + ReadOnlyMemory expectedMemory, + ReadOnlyMemory actualMemory, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false) + { + GuardArgumentNotNull(nameof(expectedMemory), expectedMemory); + + Equal( + expectedMemory.Span, + actualMemory.Span, + ignoreCase, + ignoreLineEndingDifferences, + ignoreWhiteSpaceDifferences + ); + } + + /// + /// 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()); + } + } +} + +#endif diff --git a/MultipleAsserts.cs b/MultipleAsserts.cs new file mode 100644 index 0000000000000..940cea3120544 --- /dev/null +++ b/MultipleAsserts.cs @@ -0,0 +1,49 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 new MultipleException(exceptions); + } + } +} diff --git a/NullAsserts.cs b/NullAsserts.cs new file mode 100644 index 0000000000000..c237265cebb30 --- /dev/null +++ b/NullAsserts.cs @@ -0,0 +1,50 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 new NotNullException(); + } + + /// + /// 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 new NullException(@object); + } + } +} diff --git a/PropertyAsserts.cs b/PropertyAsserts.cs new file mode 100644 index 0000000000000..4a63a7c09503a --- /dev/null +++ b/PropertyAsserts.cs @@ -0,0 +1,100 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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; + + PropertyChangedEventHandler handler = (sender, args) => propertyChangeHappened |= string.IsNullOrEmpty(args.PropertyName) || propertyName.Equals(args.PropertyName, StringComparison.OrdinalIgnoreCase); + + @object.PropertyChanged += handler; + + try + { + testCode(); + if (!propertyChangeHappened) + throw new PropertyChangedException(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 NotImplementedException(); + } + + /// + /// 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; + + PropertyChangedEventHandler handler = (sender, args) => propertyChangeHappened |= string.IsNullOrEmpty(args.PropertyName) || propertyName.Equals(args.PropertyName, StringComparison.OrdinalIgnoreCase); + + @object.PropertyChanged += handler; + + try + { + await testCode(); + if (!propertyChangeHappened) + throw new PropertyChangedException(propertyName); + } + finally + { + @object.PropertyChanged -= handler; + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000000000..78f95e940f44f --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# About This Project + +This project contains the xUnit.net assertion library source code, intended to be used as a Git submodule. Code here is built with a target-framework of `netstandard1.1`, and must support both `net452` and `netcoreapp1.0`. The code must be buildable by a minimum of C# 6.0. These constraints are supported by the [suggested contribution workflow](#suggested-contribution-workflow), which makes it trivial to know when you've used unavailable features. + +> _**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_IMMUTABLE_COLLECTIONS` (min: C# 6.0, xUnit.net v2) + +There are assertions that target immutable collections. If you are using a target framework that is compatible with [`System.Collections.Immutable`](https://www.nuget.org/packages/System.Collections.Immutable), you should define `XUNIT_IMMUTABLE_COLLECTIONS` to enable the additional versions of those assertions that will consume immutable collections. + +### `XUNIT_NULLABLE` (min: C# 9.0, xUnit.net v2) + +Projects that consume this repository as source, which wish to use nullable reference type annotations should define the `XUNIT_NULLABLE` compilation symbol to opt-in to the relevant nullability analysis annotations on method signatures. + +### `XUNIT_SKIP` (min: C# 10.0, xUnit.net v3) + +The Skip family of assertions (like `Assert.Skip`) require xUnit.net v3. Define this to enable the Skip assertions. + +> _**Note**: If you enable try to use it from xUnit.net v2, the test will show up as failed rather than skipped. Runtime support in the core library is required to make this feature work properly, which is why it's not supported for v2._ + +### `XUNIT_SPAN` (min: C# 6.0, xUnit.net v2) + +There are optimized versions of `Assert.Equal` for arrays which use `Span`- and/or `Memory`-based comparison options. If you are using a target framework that supports `Span` and `Memory`, you should define `XUNIT_SPAN` to enable these new assertions. + +### `XUNIT_VALUETASK` (min: C# 6.0, xUnit.net v2) + +Any asynchronous assertion API (like `Assert.ThrowsAsync`) is available with versions that consume `Task` or `Task`. If you are using a target framework and compiler that support `ValueTask`, you should define `XUNIT_VALUETASK` to enable additional versions of those assertions that will consume `ValueTask` and/or `ValueTask`. + +### `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/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/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 + +[](https://dotnetfoundation.org/projects/project-detail/xunit) + +xUnit.net is a free, open source, community-focused unit testing tool for the .NET Framework. Written by the original inventor of NUnit v2, xUnit.net is the latest technology for unit testing C#, F#, VB.NET and other .NET languages. xUnit.net works with ReSharper, CodeRush, TestDriven.NET and Xamarin. It is part of the [.NET Foundation](https://www.dotnetfoundation.org/), and operates under their [code of conduct](http://www.dotnetfoundation.org/code-of-conduct). It is licensed under [Apache 2](https://opensource.org/licenses/Apache-2.0) (an OSI approved license). + +For project documentation, please visit the [xUnit.net project home](https://xunit.net/). diff --git a/RangeAsserts.cs b/RangeAsserts.cs new file mode 100644 index 0000000000000..f8c8f92d4c7a8 --- /dev/null +++ b/RangeAsserts.cs @@ -0,0 +1,90 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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, GetComparer()); + + /// + /// 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(comparer), comparer); + + if (comparer.Compare(low, actual) > 0 || comparer.Compare(actual, high) > 0) + throw new InRangeException(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, GetComparer()); + + /// + /// 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(comparer), comparer); + + if (comparer.Compare(low, actual) <= 0 && comparer.Compare(actual, high) <= 0) + throw new NotInRangeException(actual, low, high); + } + } +} diff --git a/Record.cs b/Record.cs new file mode 100644 index 0000000000000..96d2d34629e09 --- /dev/null +++ b/Record.cs @@ -0,0 +1,173 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + /// + /// 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 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) + { + 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. + /// Returns the exception that was thrown by the code; null, otherwise. +#if XUNIT_NULLABLE + protected static Exception? RecordException(Func testCode) +#else + protected static Exception RecordException(Func testCode) +#endif + { + GuardArgumentNotNull(nameof(testCode), testCode); + var task = default(Task); + + try + { + task = testCode() as Task; + } + catch (Exception ex) + { + return ex; + } + + if (task != null) + throw new InvalidOperationException("You must call Assert.ThrowsAsync, Assert.DoesNotThrowAsync, or Record.ExceptionAsync when testing async code."); + + 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 NotImplementedException(); + } + +#if XUNIT_VALUETASK + /// + [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 NotImplementedException(); + } + + /// + [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 NotImplementedException(); + } +#endif + + /// + /// 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 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) + { + return ex; + } + } + +#if XUNIT_VALUETASK + /// + /// 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 XUNIT_NULLABLE + protected static async ValueTask RecordExceptionAsync(Func testCode) +#else + protected static async ValueTask RecordExceptionAsync(Func testCode) +#endif + { + GuardArgumentNotNull(nameof(testCode), testCode); + + try + { + await testCode(); + return null; + } + catch (Exception ex) + { + return ex; + } + } + + /// + /// Records any exception which is thrown by the given task. + /// + /// The task which may thrown an exception. + /// The type of the ValueTask return value. + /// Returns the exception that was thrown by the code; null, otherwise. +#if XUNIT_NULLABLE + protected static async ValueTask RecordExceptionAsync(Func> testCode) +#else + protected static async ValueTask RecordExceptionAsync(Func> testCode) +#endif + { + GuardArgumentNotNull(nameof(testCode), testCode); + + try + { + await testCode(); + return null; + } + catch (Exception ex) + { + return ex; + } + } +#endif + } +} diff --git a/Sdk/ArgumentFormatter.cs b/Sdk/ArgumentFormatter.cs new file mode 100644 index 0000000000000..b83dedca5b696 --- /dev/null +++ b/Sdk/ArgumentFormatter.cs @@ -0,0 +1,484 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Xunit.Sdk +{ + /// + /// Formats arguments for display in theories. + /// + static class ArgumentFormatter + { + const int MAX_DEPTH = 3; + const int MAX_ENUMERABLE_LENGTH = 5; + const int MAX_OBJECT_PARAMETER_COUNT = 5; + const int MAX_STRING_LENGTH = 50; + + static readonly object[] EmptyObjects = new object[0]; + static readonly Type[] EmptyTypes = new Type[0]; + + // List of system types => C# type names + static readonly Dictionary TypeMappings = new Dictionary + { + { typeof(bool).GetTypeInfo(), "bool" }, + { typeof(byte).GetTypeInfo(), "byte" }, + { typeof(sbyte).GetTypeInfo(), "sbyte" }, + { typeof(char).GetTypeInfo(), "char" }, + { typeof(decimal).GetTypeInfo(), "decimal" }, + { typeof(double).GetTypeInfo(), "double" }, + { typeof(float).GetTypeInfo(), "float" }, + { typeof(int).GetTypeInfo(), "int" }, + { typeof(uint).GetTypeInfo(), "uint" }, + { typeof(long).GetTypeInfo(), "long" }, + { typeof(ulong).GetTypeInfo(), "ulong" }, + { typeof(object).GetTypeInfo(), "object" }, + { typeof(short).GetTypeInfo(), "short" }, + { typeof(ushort).GetTypeInfo(), "ushort" }, + { typeof(string).GetTypeInfo(), "string" }, + }; + + /// + /// Format the value for presentation. + /// + /// The value to be formatted. + /// The position where the difference starts + /// + /// The formatted value. + public static string Format( +#if XUNIT_NULLABLE + object? value, +#else + object value, +#endif + out int? pointerPosition, + int? errorIndex = null) + { + return Format(value, 1, out pointerPosition, errorIndex); + } + + /// + /// Format the value for presentation. + /// + /// The value to be formatted. + /// + /// The formatted value. + public static string Format( +#if XUNIT_NULLABLE + object? value, +#else + object value, +#endif + int? errorIndex = null) + { + int? _; + + return Format(value, 1, out _, errorIndex); + } + + static string FormatInner( +#if XUNIT_NULLABLE + object? value, +#else + object value, +#endif + int depth) + { + int? _; + + return Format(value, depth, out _, null); + } + + static string Format( +#if XUNIT_NULLABLE + object? value, +#else + object value, +#endif + int depth, + out int? pointerPostion, + int? errorIndex = null) + { + pointerPostion = null; + + if (value == null) + return "null"; + + var valueAsType = value as Type; + if (valueAsType != null) + return $"typeof({FormatTypeName(valueAsType)})"; + + try + { + if (value.GetType().GetTypeInfo().IsEnum) + return value.ToString()?.Replace(", ", " | ") ?? "null"; + + if (value is char) + { + var charValue = (char)value; + + if (charValue == '\'') + return @"'\''"; + + // Take care of all of the escape sequences +#if XUNIT_NULLABLE + string? escapeSequence; +#else + string escapeSequence; +#endif + if (TryGetEscapeSequence(charValue, out escapeSequence)) + return $"'{escapeSequence}'"; + + if (char.IsLetterOrDigit(charValue) || char.IsPunctuation(charValue) || char.IsSymbol(charValue) || charValue == ' ') + return $"'{charValue}'"; + + // Fallback to hex + return $"0x{(int)charValue:x4}"; + } + + if (value is DateTime || value is DateTimeOffset) + return $"{value:o}"; + + var stringParameter = value as string; + if (stringParameter != null) + { + stringParameter = EscapeString(stringParameter); + stringParameter = stringParameter.Replace(@"""", @"\"""); // escape double quotes + if (stringParameter.Length > MAX_STRING_LENGTH) + { + var displayed = stringParameter.Substring(0, MAX_STRING_LENGTH); + return $"\"{displayed}\"..."; + } + + return $"\"{stringParameter}\""; + } + + try + { + var enumerable = value as IEnumerable; + if (enumerable != null) + return FormatEnumerable(enumerable.Cast(), depth, errorIndex, out pointerPostion); + } + catch + { + // Sometimes enumerables cannot be enumerated when being, and instead thrown an exception. + // This could be, for example, because they require state that is not provided by Xunit. + // In these cases, just continue formatting. + } + + if (value is float) + return $"{value:G9}"; + + if (value is double) + return $"{value:G17}"; + + var type = value.GetType(); + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsValueType) + { + if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) + { + var k = typeInfo.GetDeclaredProperty("Key")?.GetValue(value, null); + var v = typeInfo.GetDeclaredProperty("Value")?.GetValue(value, null); + + return $"[{Format(k)}] = {Format(v)}"; + } + + return Convert.ToString(value, CultureInfo.CurrentCulture) ?? "null"; + } + + var task = value as Task; + if (task != null) + { + var typeParameters = typeInfo.GenericTypeArguments; + var typeName = typeParameters.Length == 0 ? "Task" : $"Task<{string.Join(",", typeParameters.Select(FormatTypeName))}>"; + return $"{typeName} {{ Status = {task.Status} }}"; + } + + var toString = type.GetRuntimeMethod("ToString", EmptyTypes); + + if (toString != null && toString.DeclaringType != typeof(object)) +#if XUNIT_NULLABLE + return ((string?)toString.Invoke(value, EmptyObjects)) ?? "null"; +#else + return ((string)toString.Invoke(value, EmptyObjects)) ?? "null"; +#endif + + return FormatComplexValue(value, depth, type); + } + catch (Exception ex) + { + // Sometimes an exception is thrown when formatting an argument, such as in ToString. + // In these cases, we don't want xunit to crash, as tests may have passed despite this. + return $"{ex.GetType().Name} was thrown formatting an object of type \"{value.GetType()}\""; + } + } + + static string FormatComplexValue( + object value, + int depth, + Type type) + { + if (depth == MAX_DEPTH) + return $"{type.Name} {{ ... }}"; + + var fields = + type + .GetRuntimeFields() + .Where(f => f.IsPublic && !f.IsStatic) + .Select(f => new { name = f.Name, value = WrapAndGetFormattedValue(() => f.GetValue(value), depth) }); + + 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) }); + + var parameters = + fields + .Concat(properties) + .OrderBy(p => p.name) + .Take(MAX_OBJECT_PARAMETER_COUNT + 1) + .ToList(); + + if (parameters.Count == 0) + return $"{type.Name} {{ }}"; + + var formattedParameters = string.Join(", ", parameters.Take(MAX_OBJECT_PARAMETER_COUNT).Select(p => $"{p.name} = {p.value}")); + + if (parameters.Count > MAX_OBJECT_PARAMETER_COUNT) + formattedParameters += ", ..."; + + return $"{type.Name} {{ {formattedParameters} }}"; + } + + static string FormatEnumerable( + IEnumerable enumerableValues, + int depth, + int? neededIndex, + out int? pointerPostion) + { + pointerPostion = null; + + if (depth == MAX_DEPTH) + return "[...]"; + + var printedValues = string.Empty; + + if (neededIndex.HasValue) + { + var enumeratedValues = enumerableValues.ToList(); + + var half = (int)Math.Floor(MAX_ENUMERABLE_LENGTH / 2m); + var startIndex = Math.Max(0, neededIndex.Value - half); + var endIndex = Math.Min(enumeratedValues.Count, startIndex + MAX_ENUMERABLE_LENGTH); + startIndex = Math.Max(0, endIndex - MAX_ENUMERABLE_LENGTH); + + var leftCount = neededIndex.Value - startIndex; + + if (startIndex != 0) + printedValues += "..., "; + + var leftValues = enumeratedValues.Skip(startIndex).Take(leftCount).ToList(); + var rightValues = enumeratedValues.Skip(startIndex + leftCount).Take(MAX_ENUMERABLE_LENGTH - leftCount + 1).ToList(); + + // Values to the left of the difference + if (leftValues.Count > 0) + { + printedValues += string.Join(", ", leftValues.Select(x => FormatInner(x, depth + 1))); + + if (rightValues.Count > 0) + printedValues += ", "; + } + + pointerPostion = printedValues.Length + 1; + + // Difference value and values to the right + printedValues += string.Join(", ", rightValues.Take(MAX_ENUMERABLE_LENGTH - leftCount).Select(x => FormatInner(x, depth + 1))); + if (leftValues.Count + rightValues.Count > MAX_ENUMERABLE_LENGTH) + printedValues += ", ..."; + } + else + { + var values = enumerableValues.Take(MAX_ENUMERABLE_LENGTH + 1).ToList(); + printedValues += string.Join(", ", values.Take(MAX_ENUMERABLE_LENGTH).Select(x => FormatInner(x, depth + 1))); + if (values.Count > MAX_ENUMERABLE_LENGTH) + printedValues += ", ..."; + } + + return $"[{printedValues}]"; + } + + static bool IsSZArrayType(this TypeInfo typeInfo) + { +#if NETCOREAPP2_0_OR_GREATER + return typeInfo.IsSZArray; +#elif XUNIT_NULLABLE + return typeInfo == typeInfo.GetElementType()!.MakeArrayType().GetTypeInfo(); +#else + return typeInfo == typeInfo.GetElementType().MakeArrayType().GetTypeInfo(); +#endif + } + + static string FormatTypeName(Type type) + { + var typeInfo = type.GetTypeInfo(); + var arraySuffix = ""; + + // Deconstruct and re-construct array + while (typeInfo.IsArray) + { + if (typeInfo.IsSZArrayType()) + arraySuffix += "[]"; + else + { + var rank = typeInfo.GetArrayRank(); + if (rank == 1) + arraySuffix += "[*]"; + else + arraySuffix += $"[{new string(',', rank - 1)}]"; + } + +#if XUNIT_NULLABLE + typeInfo = typeInfo.GetElementType()!.GetTypeInfo(); +#else + typeInfo = typeInfo.GetElementType().GetTypeInfo(); +#endif + } + + // Map C# built-in type names +#if XUNIT_NULLABLE + string? result; +#else + string result; +#endif + if (TypeMappings.TryGetValue(typeInfo, out result)) + return result + arraySuffix; + + // Strip off generic suffix + var name = typeInfo.FullName; + + // catch special case of generic parameters not being bound to a specific type: + if (name == null) + return typeInfo.Name; + + var tickIdx = name.IndexOf('`'); + if (tickIdx > 0) + name = name.Substring(0, tickIdx); + + if (typeInfo.IsGenericTypeDefinition) + name = $"{name}<{new string(',', typeInfo.GenericTypeParameters.Length - 1)}>"; + else if (typeInfo.IsGenericType) + { + if (typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) + name = FormatTypeName(typeInfo.GenericTypeArguments[0]) + "?"; + else + name = $"{name}<{string.Join(", ", typeInfo.GenericTypeArguments.Select(FormatTypeName))}>"; + } + + return name + arraySuffix; + } + + static string WrapAndGetFormattedValue( +#if XUNIT_NULLABLE + Func getter, +#else + Func getter, +#endif + int depth) + { + try + { + return FormatInner(getter(), depth + 1); + } + catch (Exception ex) + { + return $"(throws {UnwrapException(ex)?.GetType().Name})"; + } + } + + static Exception UnwrapException(Exception ex) + { + while (true) + { + var tiex = ex as TargetInvocationException; + if (tiex == null || tiex.InnerException == null) + return ex; + + ex = tiex.InnerException; + } + } + + internal static string EscapeString(string s) + { + var builder = new StringBuilder(s.Length); + for (var i = 0; i < s.Length; i++) + { + var ch = s[i]; +#if XUNIT_NULLABLE + string? escapeSequence; +#else + string escapeSequence; +#endif + if (TryGetEscapeSequence(ch, out escapeSequence)) + builder.Append(escapeSequence); + else if (ch < 32) // C0 control char + builder.AppendFormat(@"\x{0}", (+ch).ToString("x2")); + 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(@"\x{0}", (+ch).ToString("x4")); + } + else + builder.Append(ch); // Append the char like normal + } + return builder.ToString(); + } + + 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; + } + } +} diff --git a/Sdk/AssertComparer.cs b/Sdk/AssertComparer.cs new file mode 100644 index 0000000000000..7ced51ac97fe6 --- /dev/null +++ b/Sdk/AssertComparer.cs @@ -0,0 +1,52 @@ +#if XUNIT_NULLABLE +#nullable enable +#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. + class AssertComparer : 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? + var comparable1 = x as IComparable; + if (comparable1 != null) + return comparable1.CompareTo(y); + + // Implements IComparable + return x.CompareTo(y); + } + } +} diff --git a/Sdk/AssertEqualityComparer.cs b/Sdk/AssertEqualityComparer.cs new file mode 100644 index 0000000000000..d37bca9d3595a --- /dev/null +++ b/Sdk/AssertEqualityComparer.cs @@ -0,0 +1,431 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Sdk +{ + /// + /// Default implementation of used by the xUnit.net equality assertions. + /// + /// The type that is being compared. + class AssertEqualityComparer : IEqualityComparer + { + static readonly IEqualityComparer DefaultInnerComparer = new AssertEqualityComparerAdapter(new AssertEqualityComparer()); + static readonly TypeInfo NullableTypeInfo = typeof(Nullable<>).GetTypeInfo(); + + readonly Func innerComparerFactory; + + /// + /// 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 + innerComparerFactory = () => innerComparer ?? DefaultInnerComparer; + } + + /// + public bool Equals( +#if XUNIT_NULLABLE + [AllowNull] T x, + [AllowNull] T y) +#else + T x, + T y) +#endif + { + int? _; + + return Equals(x, y, out _); + } + + /// + public bool Equals( +#if XUNIT_NULLABLE + [AllowNull] T x, + [AllowNull] T y, +#else + T x, + T y, +#endif + out int? mismatchIndex) + { + mismatchIndex = null; + var typeInfo = typeof(T).GetTypeInfo(); + + // Null? + if (x == null && y == null) + return true; + if (x == null || y == null) + return false; + + // Implements IEquatable? + var equatable = x as IEquatable; + if (equatable != null) + return equatable.Equals(y); + + // Implements IComparable? + var comparableGeneric = x as IComparable; + if (comparableGeneric != null) + { + try + { + return comparableGeneric.CompareTo(y) == 0; + } + 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? + var comparable = x as IComparable; + if (comparable != null) + { + try + { + return comparable.CompareTo(y) == 0; + } + 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. + } + } + + // Dictionaries? + var dictionariesEqual = CheckIfDictionariesAreEqual(x, y); + if (dictionariesEqual.HasValue) + return dictionariesEqual.GetValueOrDefault(); + + // Sets? + var setsEqual = CheckIfSetsAreEqual(x, y, typeInfo); + if (setsEqual.HasValue) + return setsEqual.GetValueOrDefault(); + + // Enumerable? + var enumerablesEqual = CheckIfEnumerablesAreEqual(x, y, out mismatchIndex); + if (enumerablesEqual.HasValue) + { + if (!enumerablesEqual.GetValueOrDefault()) + { + return false; + } + + // Array.GetEnumerator() flattens out the array, ignoring array ranks and lengths + var xArray = x as Array; + var yArray = y as Array; + if (xArray != null && yArray != null) + { + // new object[2,1] != new object[2] + if (xArray.Rank != yArray.Rank) + return false; + + // new object[2,1] != new object[1,2] + for (var i = 0; i < xArray.Rank; i++) + if (xArray.GetLength(i) != yArray.GetLength(i)) + return false; + } + + return true; + } + + // Implements IStructuralEquatable? + var structuralEquatable = x as IStructuralEquatable; + if (structuralEquatable != null && structuralEquatable.Equals(y, new TypeErasedEqualityComparer(innerComparerFactory()))) + return true; + + // Implements IEquatable? + var iequatableY = typeof(IEquatable<>).MakeGenericType(y.GetType()).GetTypeInfo(); + if (iequatableY.IsAssignableFrom(x.GetType().GetTypeInfo())) + { + var equalsMethod = iequatableY.GetDeclaredMethod(nameof(IEquatable.Equals)); + if (equalsMethod == null) + return false; + +#if XUNIT_NULLABLE + return equalsMethod.Invoke(x, new object[] { y }) is true; +#else + return (bool)equalsMethod.Invoke(x, new object[] { y }); +#endif + } + + // Implements IComparable? + var icomparableY = typeof(IComparable<>).MakeGenericType(y.GetType()).GetTypeInfo(); + if (icomparableY.IsAssignableFrom(x.GetType().GetTypeInfo())) + { + var compareToMethod = icomparableY.GetDeclaredMethod(nameof(IComparable.CompareTo)); + if (compareToMethod == null) + return false; + + try + { +#if XUNIT_NULLABLE + return compareToMethod.Invoke(x, new object[] { y }) is 0; +#else + return (int)compareToMethod.Invoke(x, new object[] { y }) == 0; +#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. + } + } + + // Last case, rely on object.Equals + return object.Equals(x, y); + } + + bool? CheckIfEnumerablesAreEqual( +#if XUNIT_NULLABLE + [AllowNull] T x, + [AllowNull] T y, +#else + T x, + T y, +#endif + out int? mismatchIndex) + { + mismatchIndex = null; + + var enumerableX = x as IEnumerable; + var enumerableY = y as IEnumerable; + + if (enumerableX == null || enumerableY == null) + return null; + + var enumeratorX = default(IEnumerator); + var enumeratorY = default(IEnumerator); + + try + { + enumeratorX = enumerableX.GetEnumerator(); + enumeratorY = enumerableY.GetEnumerator(); + var equalityComparer = innerComparerFactory(); + + mismatchIndex = 0; + + while (true) + { + var hasNextX = enumeratorX.MoveNext(); + var hasNextY = enumeratorY.MoveNext(); + + if (!hasNextX || !hasNextY) + { + if (hasNextX == hasNextY) + { + mismatchIndex = null; + return true; + } + + return false; + } + + if (!equalityComparer.Equals(enumeratorX.Current, enumeratorY.Current)) + return false; + + mismatchIndex++; + } + } + finally + { + var asDisposable = enumeratorX as IDisposable; + if (asDisposable != null) + asDisposable.Dispose(); + asDisposable = enumeratorY as IDisposable; + if (asDisposable != null) + asDisposable.Dispose(); + } + } + + bool? CheckIfDictionariesAreEqual( +#if XUNIT_NULLABLE + [AllowNull] T x, + [AllowNull] T y) +#else + T x, + T y) +#endif + { + var dictionaryX = x as IDictionary; + var dictionaryY = y as IDictionary; + + if (dictionaryX == null || dictionaryY == null) + return null; + + if (dictionaryX.Count != dictionaryY.Count) + return false; + + var equalityComparer = innerComparerFactory(); + var dictionaryYKeys = new HashSet(dictionaryY.Keys.Cast()); + + foreach (var key in dictionaryX.Keys.Cast()) + { + if (!dictionaryYKeys.Contains(key)) + return false; + + var valueX = dictionaryX[key]; + var valueY = dictionaryY[key]; + + if (!equalityComparer.Equals(valueX, valueY)) + return false; + + dictionaryYKeys.Remove(key); + } + + return dictionaryYKeys.Count == 0; + } + +#if XUNIT_NULLABLE + static MethodInfo? s_compareTypedSetsMethod; +#else + static MethodInfo s_compareTypedSetsMethod; +#endif + + bool? CheckIfSetsAreEqual( +#if XUNIT_NULLABLE + [AllowNull] T x, + [AllowNull] T y, +#else + T x, + T y, +#endif + TypeInfo typeInfo) + { + if (!IsSet(typeInfo)) + return null; + + var enumX = x as IEnumerable; + var enumY = y as IEnumerable; + if (enumX == null || enumY == null) + return null; + + Type elementType; + if (typeof(T).GenericTypeArguments.Length != 1) + elementType = typeof(object); + else + elementType = typeof(T).GenericTypeArguments[0]; + + if (s_compareTypedSetsMethod == null) + { + s_compareTypedSetsMethod = GetType().GetTypeInfo().GetDeclaredMethod(nameof(CompareTypedSets)); + if (s_compareTypedSetsMethod == null) + return false; + } + + var method = s_compareTypedSetsMethod.MakeGenericMethod(new Type[] { elementType }); +#if XUNIT_NULLABLE + return method.Invoke(this, new object[] { enumX, enumY }) is true; +#else + return (bool)method.Invoke(this, new object[] { enumX, enumY }); +#endif + } + + bool CompareTypedSets( + IEnumerable enumX, + IEnumerable enumY) + { + var setX = new HashSet(enumX.Cast()); + var setY = new HashSet(enumY.Cast()); + + return setX.SetEquals(setY); + } + + bool IsSet(TypeInfo typeInfo) => + typeInfo + .ImplementedInterfaces + .Select(i => i.GetTypeInfo()) + .Where(ti => ti.IsGenericType) + .Select(ti => ti.GetGenericTypeDefinition()) + .Contains(typeof(ISet<>).GetGenericTypeDefinition()); + + /// + public int GetHashCode(T obj) + { + throw new NotImplementedException(); + } + + private class TypeErasedEqualityComparer : IEqualityComparer + { + readonly IEqualityComparer innerComparer; + + public TypeErasedEqualityComparer(IEqualityComparer innerComparer) + { + this.innerComparer = innerComparer; + } + +#if XUNIT_NULLABLE + static MethodInfo? s_equalsMethod; +#else + static MethodInfo s_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. + Type objectType = x.GetType() == y.GetType() ? x.GetType() : typeof(object); + + // Lazily initialize and cache the EqualsGeneric method. + if (s_equalsMethod == null) + { + s_equalsMethod = typeof(TypeErasedEqualityComparer).GetTypeInfo().GetDeclaredMethod(nameof(EqualsGeneric)); + if (s_equalsMethod == null) + return false; + } + +#if XUNIT_NULLABLE + return s_equalsMethod.MakeGenericMethod(objectType).Invoke(this, new object[] { x, y }) is true; +#else + return (bool)s_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) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/Sdk/AssertEqualityComparerAdapter.cs b/Sdk/AssertEqualityComparerAdapter.cs new file mode 100644 index 0000000000000..d3314bb005546 --- /dev/null +++ b/Sdk/AssertEqualityComparerAdapter.cs @@ -0,0 +1,49 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Xunit.Sdk +{ + /// + /// A class that wraps to create . + /// + /// The type that is being compared. + class AssertEqualityComparerAdapter : IEqualityComparer + { + readonly IEqualityComparer innerComparer; + + /// + /// Initializes a new instance of the class. + /// + /// The comparer that is being adapted. + public AssertEqualityComparerAdapter(IEqualityComparer innerComparer) + { + if (innerComparer == null) + throw new ArgumentNullException(nameof(innerComparer)); + + this.innerComparer = 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 int GetHashCode(object obj) + { + throw new NotImplementedException(); + } + } +} diff --git a/Sdk/AssertHelper.cs b/Sdk/AssertHelper.cs new file mode 100644 index 0000000000000..2c598670030f2 --- /dev/null +++ b/Sdk/AssertHelper.cs @@ -0,0 +1,234 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Internal +{ + internal static class AssertHelper + { +#if XUNIT_NULLABLE + static ConcurrentDictionary>> gettersByType = new ConcurrentDictionary>>(); +#else + static ConcurrentDictionary>> gettersByType = new ConcurrentDictionary>>(); +#endif + +#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 && p.GetIndexParameters().Length == 0) +#if XUNIT_NULLABLE + .Select(p => new { name = p.Name, getter = (Func)p.GetValue }); +#else + .Select(p => new { name = p.Name, getter = (Func)p.GetValue }); +#endif + + return + fieldGetters + .Concat(propertyGetters) + .ToDictionary(g => g.name, g => g.getter); + }); + +#if XUNIT_NULLABLE + public static EquivalentException? VerifyEquivalence( + object? expected, + object? actual, +#else + public static EquivalentException VerifyEquivalence( + object expected, + object actual, +#endif + bool strict) + { + return VerifyEquivalence(expected, actual, strict, string.Empty, new HashSet(), new HashSet()); + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalence( + object? expected, + object? actual, +#else + public static EquivalentException VerifyEquivalence( + object expected, + object actual, +#endif + bool strict, + string prefix, + HashSet expectedRefs, + HashSet actualRefs) + { + // 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 (object.ReferenceEquals(expected, actual)) + return null; + + // Prevent circular references + if (expectedRefs.Contains(expected)) + return EquivalentException.ForCircularReference($"{nameof(expected)}.{prefix}"); + + if (actualRefs.Contains(actual)) + return EquivalentException.ForCircularReference($"{nameof(actual)}.{prefix}"); + + expectedRefs.Add(expected); + actualRefs.Add(actual); + + try + { + var expectedType = expected.GetType(); + var expectedTypeInfo = expectedType.GetTypeInfo(); + + // Primitive types, enums and strings should just fall back to their Equals implementation + if (expectedTypeInfo.IsPrimitive || expectedTypeInfo.IsEnum || expectedType == typeof(string)) + return + expected.Equals(actual) + ? null + : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + + // IComparable value types should fall back to their CompareTo implementation + if (expectedTypeInfo.IsValueType) + { + var expectedComparable = expected as IComparable; + if (expectedComparable != null) + return + expectedComparable.CompareTo(actual) == 0 + ? null + : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + } + + // Enumerables? Check equivalence of individual members + var enumerableExpected = expected as IEnumerable; + var enumerableActual = actual as IEnumerable; + if (enumerableExpected != null && enumerableActual != null) + return VerifyEquivalenceEnumerable(enumerableExpected, enumerableActual, strict, prefix, expectedRefs, actualRefs); + + return VerifyEquivalenceReference(expected, actual, strict, prefix, expectedRefs, actualRefs); + } + finally + { + expectedRefs.Remove(expected); + actualRefs.Remove(actual); + } + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceEnumerable( +#else + static EquivalentException VerifyEquivalenceEnumerable( +#endif + IEnumerable expected, + IEnumerable actual, + bool strict, + string prefix, + HashSet expectedRefs, + HashSet actualRefs) + { +#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(); + + // 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, "", expectedRefs, actualRefs) == null) + break; + + if (actualIdx == actualValues.Count) + return EquivalentException.ForMissingCollectionValue(expectedValue, actualOriginalValues, prefix); + + actualValues.RemoveAt(actualIdx); + } + + if (strict && actualValues.Count != 0) + return EquivalentException.ForExtraCollectionValue(expectedValues, actualOriginalValues, actualValues, prefix); + + return null; + } + +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceReference( +#else + static EquivalentException VerifyEquivalenceReference( +#endif + object expected, + object actual, + bool strict, + string prefix, + HashSet expectedRefs, + HashSet actualRefs) + { + var prefixDot = prefix == string.Empty ? 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); + + foreach (var kvp in expectedGetters) + { +#if XUNIT_NULLABLE + Func? actualGetter; +#else + Func actualGetter; +#endif + + if (!actualGetters.TryGetValue(kvp.Key, out 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); + if (ex != null) + return ex; + } + + return null; + } + } +} diff --git a/Sdk/DynamicSkipToken.cs b/Sdk/DynamicSkipToken.cs new file mode 100644 index 0000000000000..43d235b930bb9 --- /dev/null +++ b/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/Sdk/Exceptions/AllException.cs b/Sdk/Exceptions/AllException.cs new file mode 100644 index 0000000000000..8b2d7e86b55e8 --- /dev/null +++ b/Sdk/Exceptions/AllException.cs @@ -0,0 +1,70 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when an All assertion has one or more items fail an assertion. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class AllException : XunitException + { +#if XUNIT_NULLABLE + readonly IReadOnlyList> errors; +#else + readonly IReadOnlyList> errors; +#endif + readonly int totalItems; + + /// + /// Creates a new instance of the class. + /// + /// The total number of items that were in the collection. + /// The list of errors that occurred during the test pass. + public AllException( +#if XUNIT_NULLABLE + int totalItems, + Tuple[] errors) : +#else + int totalItems, + Tuple[] errors) : +#endif + base("Assert.All() Failure") + { + this.errors = errors; + this.totalItems = totalItems; + } + + /// + /// The errors that occurred during execution of the test. + /// + public IReadOnlyList Failures => + errors.Select(t => t.Item3).ToList(); + + /// + public override string Message + { + get + { + var formattedErrors = errors.Select(error => + { + var indexString = $"[{error.Item1}]: "; + var spaces = Environment.NewLine + "".PadRight(indexString.Length); + + return $"{indexString}Item: {error.Item2?.ToString()?.Replace(Environment.NewLine, spaces)}{spaces}{error.Item3.ToString().Replace(Environment.NewLine, spaces)}"; + }); + + return $"{base.Message}: {errors.Count} out of {totalItems} items in the collection did not pass.{Environment.NewLine}{string.Join(Environment.NewLine, formattedErrors)}"; + } + } + } +} diff --git a/Sdk/Exceptions/AssertActualExpectedException.cs b/Sdk/Exceptions/AssertActualExpectedException.cs new file mode 100644 index 0000000000000..3321dbc73f8a9 --- /dev/null +++ b/Sdk/Exceptions/AssertActualExpectedException.cs @@ -0,0 +1,173 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Linq; +using System.Reflection; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit.Sdk +{ + /// + /// Base class for exceptions that have actual and expected values + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class AssertActualExpectedException : XunitException + { + /// + /// Creates a new instance of the class. + /// + /// The expected value + /// The actual value + /// The user message to be shown + /// The title to use for the expected value (defaults to "Expected") + /// The title to use for the actual value (defaults to "Actual") + public AssertActualExpectedException( +#if XUNIT_NULLABLE + object? expected, + object? actual, + string? userMessage, + string? expectedTitle = null, + string? actualTitle = null) : +#else + object expected, + object actual, + string userMessage, + string expectedTitle = null, + string actualTitle = null) : +#endif + this(expected, actual, userMessage, expectedTitle, actualTitle, null) + { } + + /// + /// Creates a new instance of the class. + /// + /// The expected value + /// The actual value + /// The user message to be shown + /// The title to use for the expected value (defaults to "Expected") + /// The title to use for the actual value (defaults to "Actual") + /// The inner exception. + public AssertActualExpectedException( +#if XUNIT_NULLABLE + object? expected, + object? actual, + string? userMessage, + string? expectedTitle, + string? actualTitle, + Exception? innerException) : +#else + object expected, + object actual, + string userMessage, + string expectedTitle, + string actualTitle, + Exception innerException) : +#endif + base(userMessage, innerException) + { + Actual = ConvertToString(actual); + ActualTitle = actualTitle ?? "Actual"; + Expected = ConvertToString(expected); + ExpectedTitle = expectedTitle ?? "Expected"; + + if (actual != null && + expected != null && + Actual == Expected && + actual.GetType() != expected.GetType()) + { + Actual += $" ({actual.GetType().FullName})"; + Expected += $" ({expected.GetType().FullName})"; + } + } + + /// + /// Gets the actual value. + /// +#if XUNIT_NULLABLE + public string? Actual { get; } +#else + public string Actual { get; } +#endif + + /// + /// Gets the title used for the actual value. + /// + public string ActualTitle { get; } + + /// + /// Gets the expected value. + /// +#if XUNIT_NULLABLE + public string? Expected { get; } +#else + public string Expected { get; } +#endif + + /// + /// Gets the title used for the expected value. + /// + public string ExpectedTitle { get; } + + /// + /// Gets a message that describes the current exception. Includes the expected and actual values. + /// + /// The error message that explains the reason for the exception, or an empty string(""). + /// 1 + public override string Message + { + get + { + var titleLength = Math.Max(ExpectedTitle.Length, ActualTitle.Length) + 2; // + the colon and space + var formattedExpectedTitle = (ExpectedTitle + ":").PadRight(titleLength); + var formattedActualTitle = (ActualTitle + ":").PadRight(titleLength); + var indentedNewLine = Environment.NewLine + " ".PadRight(titleLength); + + return $"{base.Message}{Environment.NewLine}{formattedExpectedTitle}{Expected?.Replace(Environment.NewLine, indentedNewLine) ?? "(null)"}{Environment.NewLine}{formattedActualTitle}{Actual?.Replace(Environment.NewLine, indentedNewLine) ?? "(null)"}"; + } + } + + static string ConvertToSimpleTypeName(TypeInfo typeInfo) + { + if (!typeInfo.IsGenericType) + return typeInfo.Name; + + var simpleNames = typeInfo.GenericTypeArguments.Select(type => ConvertToSimpleTypeName(type.GetTypeInfo())); + var backTickIdx = typeInfo.Name.IndexOf('`'); + if (backTickIdx < 0) + backTickIdx = typeInfo.Name.Length; // F# doesn't use backticks for generic type names + + return $"{typeInfo.Name.Substring(0, backTickIdx)}<{string.Join(", ", simpleNames)}>"; + } + +#if XUNIT_NULLABLE + [return: NotNullIfNotNull("value")] + static string? ConvertToString(object? value) +#else + static string ConvertToString(object value) +#endif + { + if (value == null) + return null; + + var stringValue = value as string; + if (stringValue != null) + return stringValue; + + var formattedValue = ArgumentFormatter.Format(value); + if (value is IEnumerable) + formattedValue = $"{ConvertToSimpleTypeName(value.GetType().GetTypeInfo())} {formattedValue}"; + + return formattedValue; + } + } +} diff --git a/Sdk/Exceptions/AssertCollectionCountException.cs b/Sdk/Exceptions/AssertCollectionCountException.cs new file mode 100644 index 0000000000000..9ef8cf6e6aad1 --- /dev/null +++ b/Sdk/Exceptions/AssertCollectionCountException.cs @@ -0,0 +1,28 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when the collection did not contain exactly the given number element. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class AssertCollectionCountException : XunitException + { + /// + /// Initializes a new instance of the class. + /// + /// The expected number of items in the collection. + /// The actual number of items in the collection. + public AssertCollectionCountException( + int expectedCount, + int actualCount) : + base($"The collection contained {actualCount} matching element(s) instead of {expectedCount}.") + { } + } +} diff --git a/Sdk/Exceptions/CollectionException.cs b/Sdk/Exceptions/CollectionException.cs new file mode 100644 index 0000000000000..c01d037dd24b9 --- /dev/null +++ b/Sdk/Exceptions/CollectionException.cs @@ -0,0 +1,131 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Linq; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when Assert.Collection fails. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class CollectionException : XunitException + { +#if XUNIT_NULLABLE + readonly string? innerException; + readonly string? innerStackTrace; +#else + readonly string innerException; + readonly string innerStackTrace; +#endif + + /// + /// Creates a new instance of the class. + /// + /// The collection that failed the test. + /// The expected number of items in the collection. + /// The actual number of items in the collection. + /// The index of the position where the first comparison failure occurred. + /// The exception that was thrown during the comparison failure. + public CollectionException( +#if XUNIT_NULLABLE + object? collection, + int expectedCount, + int actualCount, + int indexFailurePoint = -1, + Exception? innerException = null) : +#else + object collection, + int expectedCount, + int actualCount, + int indexFailurePoint = -1, + Exception innerException = null) : +#endif + base("Assert.Collection() Failure") + { + Collection = collection; + ExpectedCount = expectedCount; + ActualCount = actualCount; + IndexFailurePoint = indexFailurePoint; + this.innerException = FormatInnerException(innerException); + innerStackTrace = innerException == null ? null : innerException.StackTrace; + } + + /// + /// The collection that failed the test. + /// +#if XUNIT_NULLABLE + public object? Collection { get; set; } +#else + public object Collection { get; set; } +#endif + + /// + /// The actual number of items in the collection. + /// + public int ActualCount { get; set; } + + /// + /// The expected number of items in the collection. + /// + public int ExpectedCount { get; set; } + + /// + /// The index of the position where the first comparison failure occurred, or -1 if + /// comparisions did not occur (because the actual and expected counts differed). + /// + public int IndexFailurePoint { get; set; } + + /// + public override string Message + { + get + { + if (IndexFailurePoint >= 0) + return $"{base.Message}{Environment.NewLine}Collection: {ArgumentFormatter.Format(Collection)}{Environment.NewLine}Error during comparison of item at index {IndexFailurePoint}{Environment.NewLine}Inner exception: {innerException}"; + + return $"{base.Message}{Environment.NewLine}Collection: {ArgumentFormatter.Format(Collection)}{Environment.NewLine}Expected item count: {ExpectedCount}{Environment.NewLine}Actual item count: {ActualCount}"; + } + } + + /// +#if XUNIT_NULLABLE + public override string? StackTrace +#else + public override string StackTrace +#endif + { + get + { + if (innerStackTrace == null) + return base.StackTrace; + + return innerStackTrace + Environment.NewLine + base.StackTrace; + } + } + +#if XUNIT_NULLABLE + static string? FormatInnerException(Exception? innerException) +#else + static string FormatInnerException(Exception innerException) +#endif + { + if (innerException == null) + return null; + + var lines = + innerException + .Message + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select((value, idx) => idx > 0 ? " " + value : value); + + return string.Join(Environment.NewLine, lines); + } + } +} diff --git a/Sdk/Exceptions/ContainsDuplicateException.cs b/Sdk/Exceptions/ContainsDuplicateException.cs new file mode 100644 index 0000000000000..ed1528c2e3338 --- /dev/null +++ b/Sdk/Exceptions/ContainsDuplicateException.cs @@ -0,0 +1,59 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when + /// or + /// finds a duplicate entry in the collection + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class ContainsDuplicateException : XunitException + { + + /// + /// Creates a new instance of the class. + /// + /// The object that was present twice in the collection. + /// The collection that was checked for duplicate entries. + public ContainsDuplicateException( +#if XUNIT_NULLABLE + object? duplicateObject, + IEnumerable collection) : +#else + object duplicateObject, + IEnumerable collection) : +#endif + base("Assert.Distinct() Failure") + { + DuplicateObject = duplicateObject; + Collection = collection; + } + + /// + /// Gets the collection that was checked for duplicate entries. + /// + public IEnumerable Collection { get; } + + /// + /// Gets the object that was present more than once in the collection. + /// +#if XUNIT_NULLABLE + public object? DuplicateObject { get; } +#else + public object DuplicateObject { get; } +#endif + + /// + public override string Message => + $"{base.Message}: The item {ArgumentFormatter.Format(DuplicateObject)} occurs multiple times in {ArgumentFormatter.Format(Collection)}."; + } +} diff --git a/Sdk/Exceptions/ContainsException.cs b/Sdk/Exceptions/ContainsException.cs new file mode 100644 index 0000000000000..5ac67bc049af8 --- /dev/null +++ b/Sdk/Exceptions/ContainsException.cs @@ -0,0 +1,33 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a collection unexpectedly does not contain the expected value. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class ContainsException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The expected object value + /// The actual value + public ContainsException( +#if XUNIT_NULLABLE + object? expected, + object? actual) : +#else + object expected, + object actual) : +#endif + base(expected, actual, "Assert.Contains() Failure", "Not found", "In value") + { } + } +} diff --git a/Sdk/Exceptions/DoesNotContainException.cs b/Sdk/Exceptions/DoesNotContainException.cs new file mode 100644 index 0000000000000..752ffae9819f3 --- /dev/null +++ b/Sdk/Exceptions/DoesNotContainException.cs @@ -0,0 +1,33 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a collection unexpectedly contains the expected value. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class DoesNotContainException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The expected object value + /// The actual value + public DoesNotContainException( +#if XUNIT_NULLABLE + object? expected, + object? actual) : +#else + object expected, + object actual) : +#endif + base(expected, actual, "Assert.DoesNotContain() Failure", "Found", "In value") + { } + } +} diff --git a/Sdk/Exceptions/DoesNotMatchException.cs b/Sdk/Exceptions/DoesNotMatchException.cs new file mode 100644 index 0000000000000..6ceaee5dc50c8 --- /dev/null +++ b/Sdk/Exceptions/DoesNotMatchException.cs @@ -0,0 +1,35 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a string unexpectedly matches a regular expression. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class DoesNotMatchException : XunitException + { + /// + /// Creates a new instance of the class. + /// + /// The regular expression pattern expected not to match + /// The actual value + public DoesNotMatchException( +#if XUNIT_NULLABLE + string expectedRegexPattern, + object? actual) : +#else + string expectedRegexPattern, + object actual) : +#endif + base($"Assert.DoesNotMatch() Failure:{Environment.NewLine}Regex: {expectedRegexPattern}{Environment.NewLine}Value: {actual}") + { } + } +} diff --git a/Sdk/Exceptions/EmptyException.cs b/Sdk/Exceptions/EmptyException.cs new file mode 100644 index 0000000000000..1157fbd228922 --- /dev/null +++ b/Sdk/Exceptions/EmptyException.cs @@ -0,0 +1,27 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a collection is unexpectedly not empty. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class EmptyException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The collection that was not empty + public EmptyException(IEnumerable collection) : + base("", ArgumentFormatter.Format(collection), "Assert.Empty() Failure") + { } + } +} diff --git a/Sdk/Exceptions/EndsWithException.cs b/Sdk/Exceptions/EndsWithException.cs new file mode 100644 index 0000000000000..ed8917c0e21ef --- /dev/null +++ b/Sdk/Exceptions/EndsWithException.cs @@ -0,0 +1,67 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a string does not end with the expected value. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class EndsWithException : XunitException + { + /// + /// Creates a new instance of the class. + /// + /// The expected value that should've ended + /// The actual value + public EndsWithException( +#if XUNIT_NULLABLE + string? expected, + string? actual) : +#else + string expected, + string actual) : +#endif + base($"Assert.EndsWith() Failure:{Environment.NewLine}Expected: {ShortenExpected(expected, actual) ?? "(null)"}{Environment.NewLine}Actual: {ShortenActual(expected, actual) ?? "(null)"}") + { } + +#if XUNIT_NULLABLE + static string? ShortenExpected( + string? expected, + string? actual) +#else + static string ShortenExpected( + string expected, + string actual) +#endif + { + if (expected == null || actual == null || actual.Length <= expected.Length) + return expected; + + return " " + expected; + } + +#if XUNIT_NULLABLE + static string? ShortenActual( + string? expected, + string? actual) +#else + static string ShortenActual( + string expected, + string actual) +#endif + { + if (expected == null || actual == null || actual.Length <= expected.Length) + return actual; + + return "···" + actual.Substring(actual.Length - expected.Length); + } + } +} diff --git a/Sdk/Exceptions/EqualException.cs b/Sdk/Exceptions/EqualException.cs new file mode 100644 index 0000000000000..b3b58e5048e32 --- /dev/null +++ b/Sdk/Exceptions/EqualException.cs @@ -0,0 +1,316 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when two values are unexpectedly not equal. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class EqualException : AssertActualExpectedException + { + static readonly Dictionary Encodings = new Dictionary + { + { '\r', "\\r" }, + { '\n', "\\n" }, + { '\t', "\\t" }, + { '\0', "\\0" } + }; + +#if XUNIT_NULLABLE + string? message; +#else + string message; +#endif + + /// + /// Creates a new instance of the class. + /// + /// The expected object value + /// The actual object value + public EqualException( +#if XUNIT_NULLABLE + object? expected, + object? actual) : +#else + object expected, + object actual) : +#endif + base(expected, actual, "Assert.Equal() Failure") + { + ActualIndex = -1; + ExpectedIndex = -1; + } + + /// + /// Creates a new instance of the class for string comparisons. + /// + /// The expected string value + /// The actual string value + /// The first index in the expected string where the strings differ + /// The first index in the actual string where the strings differ + public EqualException( +#if XUNIT_NULLABLE + string? expected, + string? actual, + int expectedIndex, + int actualIndex) : +#else + string expected, + string actual, + int expectedIndex, + int actualIndex) : +#endif + this(expected, actual, expectedIndex, actualIndex, null) + { } + + EqualException( +#if XUNIT_NULLABLE + string? expected, + string? actual, + int expectedIndex, + int actualIndex, + int? pointerPosition) : +#else + string expected, + string actual, + int expectedIndex, + int actualIndex, + int? pointerPosition) : +#endif + base(expected, actual, "Assert.Equal() Failure") + { + ActualIndex = actualIndex; + ExpectedIndex = expectedIndex; + PointerPosition = pointerPosition; + } + + EqualException( +#if XUNIT_NULLABLE + string? expected, + string? actual, + int expectedIndex, + int actualIndex, + string? expectedType, + string? actualType, + int? pointerPosition) : +#else + string expected, + string actual, + int expectedIndex, + int actualIndex, + string expectedType, + string actualType, + int? pointerPosition) : +#endif + this(expected, actual, expectedIndex, actualIndex, pointerPosition) + { + ActualType = actualType; + ExpectedType = expectedType; + } + + /// + /// Gets the index into the actual value where the values first differed. + /// Returns -1 if the difference index points were not provided. + /// + public int ActualIndex { get; } + + /// + /// Gets the index into the expected value where the values first differed. + /// Returns -1 if the difference index points were not provided. + /// + public int ExpectedIndex { get; } + + /// + /// Gets the type of the actual value of the first values differed. + /// Returns null if the type was not provided. + /// +#if XUNIT_NULLABLE + public string? ActualType { get; } +#else + public string ActualType { get; } +#endif + + /// + /// Gets the type of the expected value of the first values differed. + /// Returns null if the type was not provided. + /// +#if XUNIT_NULLABLE + public string? ExpectedType { get; } +#else + public string ExpectedType { get; } +#endif + + /// + public override string Message + { + get + { + if (message == null) + message = CreateMessage(); + + return message; + } + } + + /// + /// Gets the index of the difference between the IEnumerables when converted to a string. + /// + public int? PointerPosition { get; private set; } + + string CreateMessage() + { + if (ExpectedIndex == -1) + return base.Message; + + var undefinedType = string.IsNullOrEmpty(ActualType) || string.IsNullOrEmpty(ExpectedType); + + var actualTypeMessage = undefinedType || ExpectedType == ActualType ? string.Empty : ActualType; + var expectedTypeMessage = undefinedType || ExpectedType == ActualType ? string.Empty : ExpectedType; + + var printedExpected = ShortenAndEncode(Expected, expectedTypeMessage, PointerPosition ?? ExpectedIndex, '↓', ExpectedIndex); + var printedActual = ShortenAndEncode(Actual, actualTypeMessage, PointerPosition ?? ActualIndex, '↑', ActualIndex); + + var sb = new StringBuilder(); + sb.Append(UserMessage); + + if (!string.IsNullOrWhiteSpace(printedExpected.Item2)) + sb.AppendFormat( + CultureInfo.CurrentCulture, + "{0} {1}", + Environment.NewLine, + printedExpected.Item2 + ); + + sb.AppendFormat( + CultureInfo.CurrentCulture, + "{0}Expected: {1}{0}Actual: {2}", + Environment.NewLine, + printedExpected.Item1, + printedActual.Item1 + ); + + if (!string.IsNullOrWhiteSpace(printedActual.Item2)) + sb.AppendFormat( + CultureInfo.CurrentCulture, + "{0} {1}", + Environment.NewLine, + printedActual.Item2 + ); + + return sb.ToString(); + } + + /// + /// Creates a new instance of the class for IEnumerable comparisons. + /// + /// The expected object value + /// The actual object value + /// The first index in the expected IEnumerable where the strings differ + public static EqualException FromEnumerable( +#if XUNIT_NULLABLE + IEnumerable? expected, + IEnumerable? actual, +#else + IEnumerable expected, + IEnumerable actual, +#endif + int mismatchIndex) + { + int? pointerPositionExpected; + int? pointerPositionActual; + + var expectedText = ArgumentFormatter.Format(expected, out pointerPositionExpected, mismatchIndex); + var actualText = ArgumentFormatter.Format(actual, out pointerPositionActual, mismatchIndex); + var pointerPosition = (pointerPositionExpected ?? -1) > (pointerPositionActual ?? -1) ? pointerPositionExpected : pointerPositionActual; + + var expectedEnumerable = expected?.Cast(); + var actualEnumerable = actual?.Cast(); + + var expectedType = mismatchIndex < expectedEnumerable?.Count() ? expectedEnumerable.ElementAt(mismatchIndex)?.GetType().FullName : string.Empty; + var actualType = mismatchIndex < actualEnumerable?.Count() ? actualEnumerable.ElementAt(mismatchIndex)?.GetType().FullName : string.Empty; + + return new EqualException(expectedText, actualText, mismatchIndex, mismatchIndex, expectedType, actualType, pointerPosition); + } + + + static Tuple ShortenAndEncode( +#if XUNIT_NULLABLE + string? value, + string? type, +#else + string value, + string type, +#endif + int position, + char pointer, + int? index = null) + { + if (value == null) + return Tuple.Create("(null)", ""); + + index = index ?? position; + + var start = Math.Max(position - 20, 0); + var end = Math.Min(position + 41, value.Length); + var printedValue = new StringBuilder(100); + var printedPointer = new StringBuilder(100); + + if (start > 0) + { + printedValue.Append("···"); + printedPointer.Append(" "); + } + + for (var idx = start; idx < end; ++idx) + { + var c = value[idx]; + var paddingLength = 1; + +#if XUNIT_NULLABLE + string? encoding; +#else + string encoding; +#endif + + if (Encodings.TryGetValue(c, out encoding)) + { + printedValue.Append(encoding); + paddingLength = encoding.Length; + } + else + printedValue.Append(c); + + if (idx < position) + printedPointer.Append(' ', paddingLength); + else if (idx == position) + { + if (string.IsNullOrEmpty(type)) + printedPointer.AppendFormat("{0} (pos {1})", pointer, index); + else + printedPointer.AppendFormat("{0} (pos {1}, type {2})", pointer, index, type); + } + } + + if (value.Length == position) + printedPointer.AppendFormat("{0} (pos {1})", pointer, index); + + if (end < value.Length) + printedValue.Append("···"); + + return Tuple.Create(printedValue.ToString(), printedPointer.ToString()); + } + } +} diff --git a/Sdk/Exceptions/EquivalentException.cs b/Sdk/Exceptions/EquivalentException.cs new file mode 100644 index 0000000000000..0c06299867305 --- /dev/null +++ b/Sdk/Exceptions/EquivalentException.cs @@ -0,0 +1,162 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections.Generic; +using System.Linq; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when two values are unexpectedly not equal. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class EquivalentException : AssertActualExpectedException + { +#if XUNIT_NULLABLE + readonly string? message; +#else + readonly string message; +#endif + + EquivalentException(string message) : + base(null, null, null) + { + this.message = message; + } + + EquivalentException( +#if XUNIT_NULLABLE + object? expected, + object? actual, + string messageSuffix, + string? expectedTitle = null, + string? actualTitle = null) : +#else + object expected, + object actual, + string messageSuffix, + string expectedTitle = null, + string actualTitle = null) : +#endif + base(expected, actual, "Assert.Equivalent() Failure" + messageSuffix, expectedTitle, actualTitle) + { } + + /// + public override string Message => + message ?? base.Message; + + static string FormatMemberNameList( + IEnumerable memberNames, + string prefix) => + "[" + string.Join(", ", memberNames.Select(k => $"\"{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($"Assert.Equivalent() Failure: Circular reference found in '{memberName}'"); + + /// + /// 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) + { + return new EquivalentException( + FormatMemberNameList(expectedMemberNames, prefix), + FormatMemberNameList(actualMemberNames, prefix), + ": Mismatched member list" + ); + } + + /// + /// 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) + public static EquivalentException ForMemberValueMismatch( +#if XUNIT_NULLABLE + object? expected, + object? actual, +#else + object expected, + object actual, +#endif + string memberName) => + new EquivalentException( + expected, + actual, + memberName == string.Empty ? string.Empty : $": Mismatched value on member '{memberName}'" + ); + + /// + /// 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( + expected, + ArgumentFormatter.Format(actual), + $": Collection value not found{(memberName == string.Empty ? string.Empty : $" in member '{memberName}'")}", + actualTitle: "In" + ); + + /// + /// 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( + ArgumentFormatter.Format(expected), + $"{ArgumentFormatter.Format(actualLeftovers)} left over from {ArgumentFormatter.Format(actual)}", + $": Extra values found{(memberName == string.Empty ? string.Empty : $" in member '{memberName}'")}" + ); + } +} diff --git a/Sdk/Exceptions/FailException.cs b/Sdk/Exceptions/FailException.cs new file mode 100644 index 0000000000000..93d24fd1070ae --- /dev/null +++ b/Sdk/Exceptions/FailException.cs @@ -0,0 +1,25 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when the user calls .. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class FailException : XunitException + { + /// + /// Creates a new instance of the class. + /// + /// The user's failure message. + public FailException(string message) : + base($"Assert.Fail(): {message}") + { } + } +} diff --git a/Sdk/Exceptions/FalseException.cs b/Sdk/Exceptions/FalseException.cs new file mode 100644 index 0000000000000..98a444e47cc8c --- /dev/null +++ b/Sdk/Exceptions/FalseException.cs @@ -0,0 +1,32 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a value is unexpectedly true. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class FalseException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The user message to be display, or null for the default message + /// The actual value + public FalseException( +#if XUNIT_NULLABLE + string? userMessage, +#else + string userMessage, +#endif + bool? value) : + base("False", value?.ToString() ?? "(null)", userMessage ?? "Assert.False() Failure") + { } + } +} diff --git a/Sdk/Exceptions/InRangeException.cs b/Sdk/Exceptions/InRangeException.cs new file mode 100644 index 0000000000000..697240ce30fee --- /dev/null +++ b/Sdk/Exceptions/InRangeException.cs @@ -0,0 +1,74 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a value is unexpectedly not in the given range. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class InRangeException : XunitException + { + /// + /// Creates a new instance of the class. + /// + /// The actual object value + /// The low value of the range + /// The high value of the range + public InRangeException( +#if XUNIT_NULLABLE + object? actual, + object? low, + object? high) : +#else + object actual, + object low, + object high) : +#endif + base("Assert.InRange() Failure") + { + Low = low?.ToString(); + High = high?.ToString(); + Actual = actual?.ToString(); + } + + /// + /// Gets the actual object value + /// +#if XUNIT_NULLABLE + public string? Actual { get; } +#else + public string Actual { get; } +#endif + + /// + /// Gets the high value of the range + /// +#if XUNIT_NULLABLE + public string? High { get; } +#else + public string High { get; } +#endif + + /// + /// Gets the low value of the range + /// +#if XUNIT_NULLABLE + public string? Low { get; } +#else + public string Low { get; } +#endif + + /// + /// Gets a message that describes the current exception. + /// + /// The error message that explains the reason for the exception, or an empty string(""). + public override string Message => + $"{base.Message}\r\nRange: ({Low} - {High})\r\nActual: {Actual ?? "(null)"}"; + } +} diff --git a/Sdk/Exceptions/IsAssignableFromException.cs b/Sdk/Exceptions/IsAssignableFromException.cs new file mode 100644 index 0000000000000..286416d8569e1 --- /dev/null +++ b/Sdk/Exceptions/IsAssignableFromException.cs @@ -0,0 +1,34 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when the value is unexpectedly not of the given type or a derived type. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class IsAssignableFromException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The expected type + /// The actual object value + public IsAssignableFromException( + Type expected, +#if XUNIT_NULLABLE + object? actual) : +#else + object actual) : +#endif + base(expected, actual?.GetType(), "Assert.IsAssignableFrom() Failure") + { } + } +} diff --git a/Sdk/Exceptions/IsNotTypeException.cs b/Sdk/Exceptions/IsNotTypeException.cs new file mode 100644 index 0000000000000..17aa0d1df89a9 --- /dev/null +++ b/Sdk/Exceptions/IsNotTypeException.cs @@ -0,0 +1,34 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when the value is unexpectedly of the exact given type. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class IsNotTypeException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The expected type + /// The actual object value + public IsNotTypeException( + Type expected, +#if XUNIT_NULLABLE + object? actual) : +#else + object actual) : +#endif + base(expected, actual?.GetType(), "Assert.IsNotType() Failure") + { } + } +} diff --git a/Sdk/Exceptions/IsTypeException.cs b/Sdk/Exceptions/IsTypeException.cs new file mode 100644 index 0000000000000..d54e0fda7fe9c --- /dev/null +++ b/Sdk/Exceptions/IsTypeException.cs @@ -0,0 +1,33 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when the value is unexpectedly not of the exact given type. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class IsTypeException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The expected type name + /// The actual type name + public IsTypeException( +#if XUNIT_NULLABLE + string? expectedTypeName, + string? actualTypeName) : +#else + string expectedTypeName, + string actualTypeName) : +#endif + base(expectedTypeName, actualTypeName, "Assert.IsType() Failure") + { } + } +} diff --git a/Sdk/Exceptions/MatchesException.cs b/Sdk/Exceptions/MatchesException.cs new file mode 100644 index 0000000000000..b8b47094da3c7 --- /dev/null +++ b/Sdk/Exceptions/MatchesException.cs @@ -0,0 +1,35 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a string does not match a regular expression. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class MatchesException : XunitException + { + /// + /// Creates a new instance of the class. + /// + /// The expected regular expression pattern + /// The actual value + public MatchesException( +#if XUNIT_NULLABLE + string? expectedRegexPattern, + object? actual) : +#else + string expectedRegexPattern, + object actual) : +#endif + base($"Assert.Matches() Failure:{Environment.NewLine}Regex: {expectedRegexPattern}{Environment.NewLine}Value: {actual}") + { } + } +} diff --git a/Sdk/Exceptions/MultipleException.cs b/Sdk/Exceptions/MultipleException.cs new file mode 100644 index 0000000000000..6a57785f674a4 --- /dev/null +++ b/Sdk/Exceptions/MultipleException.cs @@ -0,0 +1,46 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when multiple assertions failed via . + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class MultipleException : XunitException + { + /// + /// Creates a new instance of the class. + /// + public MultipleException(IEnumerable innerExceptions) : + base("Multiple failures were encountered:") + { + if (innerExceptions == null) + throw new ArgumentNullException(nameof(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:"; + } +} diff --git a/Sdk/Exceptions/NotEmptyException.cs b/Sdk/Exceptions/NotEmptyException.cs new file mode 100644 index 0000000000000..a8c78b72cc882 --- /dev/null +++ b/Sdk/Exceptions/NotEmptyException.cs @@ -0,0 +1,24 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a collection is unexpectedly empty. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class NotEmptyException : XunitException + { + /// + /// Creates a new instance of the class. + /// + public NotEmptyException() : + base("Assert.NotEmpty() Failure") + { } + } +} diff --git a/Sdk/Exceptions/NotEqualException.cs b/Sdk/Exceptions/NotEqualException.cs new file mode 100644 index 0000000000000..e35cd1ddd45c4 --- /dev/null +++ b/Sdk/Exceptions/NotEqualException.cs @@ -0,0 +1,31 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when two values are unexpectedly equal. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class NotEqualException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + public NotEqualException( +#if XUNIT_NULLABLE + string? expected, + string? actual) : +#else + string expected, + string actual) : +#endif + base($"Not {expected ?? "(null)"}", actual ?? "(null)", "Assert.NotEqual() Failure") + { } + } +} diff --git a/Sdk/Exceptions/NotInRangeException.cs b/Sdk/Exceptions/NotInRangeException.cs new file mode 100644 index 0000000000000..c46d5dfc13142 --- /dev/null +++ b/Sdk/Exceptions/NotInRangeException.cs @@ -0,0 +1,76 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a value is unexpectedly in the given range. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class NotInRangeException : XunitException + { + /// + /// Creates a new instance of the class. + /// + /// The actual object value + /// The low value of the range + /// The high value of the range + public NotInRangeException( +#if XUNIT_NULLABLE + object? actual, + object? low, + object? high) : +#else + object actual, + object low, + object high) : +#endif + base("Assert.NotInRange() Failure") + { + Low = low?.ToString(); + High = high?.ToString(); + Actual = actual?.ToString(); + } + + /// + /// Gets the actual object value + /// +#if XUNIT_NULLABLE + public string? Actual { get; } +#else + public string Actual { get; } +#endif + + /// + /// Gets the high value of the range + /// +#if XUNIT_NULLABLE + public string? High { get; } +#else + public string High { get; } +#endif + + /// + /// Gets the low value of the range + /// +#if XUNIT_NULLABLE + public string? Low { get; } +#else + public string Low { get; } +#endif + + /// + /// Gets a message that describes the current exception. + /// + /// The error message that explains the reason for the exception, or an empty string(""). + public override string Message => + $"{base.Message}{Environment.NewLine}Range: ({Low} - {High}){Environment.NewLine}Actual: {Actual ?? "(null)"}"; + } +} diff --git a/Sdk/Exceptions/NotNullException.cs b/Sdk/Exceptions/NotNullException.cs new file mode 100644 index 0000000000000..7841fd594dd67 --- /dev/null +++ b/Sdk/Exceptions/NotNullException.cs @@ -0,0 +1,24 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when an object is unexpectedly null. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class NotNullException : XunitException + { + /// + /// Creates a new instance of the class. + /// + public NotNullException() : + base("Assert.NotNull() Failure") + { } + } +} diff --git a/Sdk/Exceptions/NotSameException.cs b/Sdk/Exceptions/NotSameException.cs new file mode 100644 index 0000000000000..4ce293dc189b3 --- /dev/null +++ b/Sdk/Exceptions/NotSameException.cs @@ -0,0 +1,24 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when two values are unexpected the same instance. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class NotSameException : XunitException + { + /// + /// Creates a new instance of the class. + /// + public NotSameException() : + base("Assert.NotSame() Failure") + { } + } +} diff --git a/Sdk/Exceptions/NullException.cs b/Sdk/Exceptions/NullException.cs new file mode 100644 index 0000000000000..103e0724d0098 --- /dev/null +++ b/Sdk/Exceptions/NullException.cs @@ -0,0 +1,25 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when an object reference is unexpectedly not null. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class NullException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The actual non-null value + public NullException(object actual) : + base(null, actual, "Assert.Null() Failure") + { } + } +} diff --git a/Sdk/Exceptions/ParameterCountMismatchException.cs b/Sdk/Exceptions/ParameterCountMismatchException.cs new file mode 100644 index 0000000000000..b4beacd1a7980 --- /dev/null +++ b/Sdk/Exceptions/ParameterCountMismatchException.cs @@ -0,0 +1,20 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception to be thrown from theory execution when the number of + /// parameter values does not the test method signature. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class ParameterCountMismatchException : Exception + { } +} diff --git a/Sdk/Exceptions/ProperSubsetException.cs b/Sdk/Exceptions/ProperSubsetException.cs new file mode 100644 index 0000000000000..3c77a8db2ca8d --- /dev/null +++ b/Sdk/Exceptions/ProperSubsetException.cs @@ -0,0 +1,36 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a set is not a proper subset of another set. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class ProperSubsetException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The expected value + /// The actual value +#if XUNIT_NULLABLE + public ProperSubsetException( + IEnumerable expected, + IEnumerable? actual) : +#else + public ProperSubsetException( + IEnumerable expected, + IEnumerable actual) : +#endif + base(expected, actual, "Assert.ProperSubset() Failure") + { } + } +} diff --git a/Sdk/Exceptions/ProperSupersetException.cs b/Sdk/Exceptions/ProperSupersetException.cs new file mode 100644 index 0000000000000..6b1bb26cbbc0e --- /dev/null +++ b/Sdk/Exceptions/ProperSupersetException.cs @@ -0,0 +1,30 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a set is not a proper superset of another set. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class ProperSupersetException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// +#if XUNIT_NULLABLE + public ProperSupersetException(IEnumerable expected, IEnumerable? actual) +#else + public ProperSupersetException(IEnumerable expected, IEnumerable actual) +#endif + : base(expected, actual, "Assert.ProperSuperset() Failure") + { } + } +} diff --git a/Sdk/Exceptions/PropertyChangedException.cs b/Sdk/Exceptions/PropertyChangedException.cs new file mode 100644 index 0000000000000..89f8cd8568f9d --- /dev/null +++ b/Sdk/Exceptions/PropertyChangedException.cs @@ -0,0 +1,26 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when code unexpectedly fails change a property. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class PropertyChangedException : XunitException + { + /// + /// Creates a new instance of the class. Call this constructor + /// when no exception was thrown. + /// + /// The name of the property that was expected to be changed. + public PropertyChangedException(string propertyName) : + base($"Assert.PropertyChanged failure: Property {propertyName} was not set") + { } + } +} diff --git a/Sdk/Exceptions/RaisesException.cs b/Sdk/Exceptions/RaisesException.cs new file mode 100644 index 0000000000000..a06bb239230b7 --- /dev/null +++ b/Sdk/Exceptions/RaisesException.cs @@ -0,0 +1,93 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Linq; +using System.Reflection; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when code unexpectedly fails to raise an event. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class RaisesException : XunitException + { +#if XUNIT_NULLABLE + readonly string? stackTrace = null; +#else + readonly string stackTrace = null; +#endif + + /// + /// Creates a new instance of the class. Call this constructor + /// when no event was raised. + /// + /// The type of the event args that was expected + public RaisesException(Type expected) + : base("(No event was raised)") + { + Expected = ConvertToSimpleTypeName(expected.GetTypeInfo()); + Actual = "(No event was raised)"; + } + + /// + /// Creates a new instance of the class. Call this constructor + /// when an + /// + /// + /// + public RaisesException(Type expected, Type actual) + : base("(Raised event did not match expected event)") + { + Expected = ConvertToSimpleTypeName(expected.GetTypeInfo()); + Actual = ConvertToSimpleTypeName(actual.GetTypeInfo()); + } + + /// + /// Gets the actual value. + /// + public string Actual { get; } + + /// + /// Gets the expected value. + /// + public string Expected { get; } + + /// + /// Gets a message that describes the current exception. Includes the expected and actual values. + /// + /// The error message that explains the reason for the exception, or an empty string(""). + /// 1 + public override string Message => + $"{base.Message}{Environment.NewLine}{Expected ?? "(null)"}{Environment.NewLine}{Actual ?? "(null)"}"; + + /// + /// Gets a string representation of the frames on the call stack at the time the current exception was thrown. + /// + /// A string that describes the contents of the call stack, with the most recent method call appearing first. +#if XUNIT_NULLABLE + public override string? StackTrace => stackTrace ?? base.StackTrace; +#else + public override string StackTrace => stackTrace ?? base.StackTrace; +#endif + + static string ConvertToSimpleTypeName(TypeInfo typeInfo) + { + if (!typeInfo.IsGenericType) + return typeInfo.Name; + + var simpleNames = typeInfo.GenericTypeArguments.Select(type => ConvertToSimpleTypeName(type.GetTypeInfo())); + var backTickIdx = typeInfo.Name.IndexOf('`'); + if (backTickIdx < 0) + backTickIdx = typeInfo.Name.Length; // F# doesn't use backticks for generic type names + + return $"{typeInfo.Name.Substring(0, backTickIdx)}<{string.Join(", ", simpleNames)}>"; + } + } +} diff --git a/Sdk/Exceptions/SameException.cs b/Sdk/Exceptions/SameException.cs new file mode 100644 index 0000000000000..6283ab201d5b8 --- /dev/null +++ b/Sdk/Exceptions/SameException.cs @@ -0,0 +1,30 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when two object references are unexpectedly not the same instance. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class SameException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The expected object reference + /// The actual object reference +#if XUNIT_NULLABLE + public SameException(object? expected, object? actual) +#else + public SameException(object expected, object actual) +#endif + : base(expected, actual, "Assert.Same() Failure") + { } + } +} diff --git a/Sdk/Exceptions/SingleException.cs b/Sdk/Exceptions/SingleException.cs new file mode 100644 index 0000000000000..e7a25ceb4d0e7 --- /dev/null +++ b/Sdk/Exceptions/SingleException.cs @@ -0,0 +1,44 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when the collection did not contain exactly one element. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class SingleException : XunitException + { + SingleException(string errorMessage) + : base(errorMessage) + { } + + /// + /// Creates an instance of for when the collection didn't contain any of the expected value. + /// +#if XUNIT_NULLABLE + public static Exception Empty(string? expected) => +#else + public static Exception Empty(string expected) => +#endif + new SingleException($"The collection was expected to contain a single element{(expected == null ? "" : " matching " + expected)}, but it {(expected == null ? "was empty." : "contained no matching elements.")}"); + + /// + /// Creates an instance of for when the collection had too many of the expected items. + /// + /// +#if XUNIT_NULLABLE + public static Exception MoreThanOne(int count, string? expected) => +#else + public static Exception MoreThanOne(int count, string expected) => +#endif + new SingleException($"The collection was expected to contain a single element{(expected == null ? "" : " matching " + expected)}, but it contained {count}{(expected == null ? "" : " matching")} elements."); + } +} diff --git a/Sdk/Exceptions/SkipException.cs b/Sdk/Exceptions/SkipException.cs new file mode 100644 index 0000000000000..a7be8bd676832 --- /dev/null +++ b/Sdk/Exceptions/SkipException.cs @@ -0,0 +1,26 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a test should be skipped. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class SkipException : XunitException + { + /// + /// Creates a new instance of the class. This is a special + /// exception that, when thrown, will cause xUnit.net to mark your test as skipped + /// rather than failed. + /// + public SkipException(string message) + : base($"{DynamicSkipToken.Value}{message}") + { } + } +} diff --git a/Sdk/Exceptions/StartsWithException.cs b/Sdk/Exceptions/StartsWithException.cs new file mode 100644 index 0000000000000..9536e41dd539f --- /dev/null +++ b/Sdk/Exceptions/StartsWithException.cs @@ -0,0 +1,48 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a string does not start with the expected value. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class StartsWithException : XunitException + { + /// + /// Creates a new instance of the class. + /// + /// The expected string value + /// The actual value +#if XUNIT_NULLABLE + public StartsWithException( + string? expected, + string? actual) : +#else + public StartsWithException( + string expected, + string actual) : +#endif + base($"Assert.StartsWith() Failure:{Environment.NewLine}Expected: {expected ?? "(null)"}{Environment.NewLine}Actual: {ShortenActual(expected, actual) ?? "(null)"}") + { } + +#if XUNIT_NULLABLE + static string? ShortenActual(string? expected, string? actual) +#else + static string ShortenActual(string expected, string actual) +#endif + { + if (expected == null || actual == null || actual.Length <= expected.Length) + return actual; + + return actual.Substring(0, expected.Length) + "..."; + } + } +} diff --git a/Sdk/Exceptions/SubsetException.cs b/Sdk/Exceptions/SubsetException.cs new file mode 100644 index 0000000000000..ff5094f7ac3dc --- /dev/null +++ b/Sdk/Exceptions/SubsetException.cs @@ -0,0 +1,30 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a set is not a subset of another set. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class SubsetException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// +#if XUNIT_NULLABLE + public SubsetException(IEnumerable expected, IEnumerable? actual) +#else + public SubsetException(IEnumerable expected, IEnumerable actual) +#endif + : base(expected, actual, "Assert.Subset() Failure") + { } + } +} diff --git a/Sdk/Exceptions/SupersetException.cs b/Sdk/Exceptions/SupersetException.cs new file mode 100644 index 0000000000000..84238a8a1539a --- /dev/null +++ b/Sdk/Exceptions/SupersetException.cs @@ -0,0 +1,30 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a set is not a superset of another set. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class SupersetException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// +#if XUNIT_NULLABLE + public SupersetException(IEnumerable expected, IEnumerable? actual) +#else + public SupersetException(IEnumerable expected, IEnumerable actual) +#endif + : base(expected, actual, "Assert.Superset() Failure") + { } + } +} diff --git a/Sdk/Exceptions/ThrowsException.cs b/Sdk/Exceptions/ThrowsException.cs new file mode 100644 index 0000000000000..afd69fc040d0a --- /dev/null +++ b/Sdk/Exceptions/ThrowsException.cs @@ -0,0 +1,78 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when code unexpectedly fails to throw an exception. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class ThrowsException : AssertActualExpectedException + { +#if XUNIT_NULLABLE + readonly string? stackTrace = null; +#else + readonly string stackTrace = null; +#endif + + /// + /// Creates a new instance of the class. Call this constructor + /// when no exception was thrown. + /// + /// The type of the exception that was expected + public ThrowsException(Type expectedType) + : this(expectedType, "(No exception was thrown)", null, null, null) + { } + + /// + /// Creates a new instance of the class. Call this constructor + /// when an exception of the wrong type was thrown. + /// + /// The type of the exception that was expected + /// The actual exception that was thrown + public ThrowsException(Type expectedType, Exception actual) +#if XUNIT_NULLABLE + : this(expectedType, ArgumentFormatter.Format(actual.GetType())!, actual.Message, actual.StackTrace, actual) +#else + : this(expectedType, ArgumentFormatter.Format(actual.GetType()), actual.Message, actual.StackTrace, actual) +#endif + { } + + /// + /// THIS CONSTRUCTOR IS FOR UNIT TESTING PURPOSES ONLY. + /// +#if XUNIT_NULLABLE + protected ThrowsException(Type expected, string actual, string? actualMessage, string? stackTrace, Exception? innerException) +#else + protected ThrowsException(Type expected, string actual, string actualMessage, string stackTrace, Exception innerException) +#endif + : base( + expected, + actual + (actualMessage == null ? "" : ": " + actualMessage), + "Assert.Throws() Failure", + null, + null, + innerException + ) + { + this.stackTrace = stackTrace; + } + + /// + /// Gets a string representation of the frames on the call stack at the time the current exception was thrown. + /// + /// A string that describes the contents of the call stack, with the most recent method call appearing first. +#if XUNIT_NULLABLE + public override string? StackTrace => stackTrace ?? base.StackTrace; +#else + public override string StackTrace => stackTrace ?? base.StackTrace; +#endif + } +} diff --git a/Sdk/Exceptions/TrueException.cs b/Sdk/Exceptions/TrueException.cs new file mode 100644 index 0000000000000..5613db684b786 --- /dev/null +++ b/Sdk/Exceptions/TrueException.cs @@ -0,0 +1,32 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +namespace Xunit.Sdk +{ + /// + /// Exception thrown when a value is unexpectedly false. + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class TrueException : AssertActualExpectedException + { + /// + /// Creates a new instance of the class. + /// + /// The user message to be displayed, or null for the default message + /// The actual value + public TrueException( +#if XUNIT_NULLABLE + string? userMessage, +#else + string userMessage, +#endif + bool? value) : + base("True", value?.ToString() ?? "(null)", userMessage ?? "Assert.True() Failure") + { } + } +} diff --git a/Sdk/Exceptions/XunitException.cs b/Sdk/Exceptions/XunitException.cs new file mode 100644 index 0000000000000..4735e9a32246c --- /dev/null +++ b/Sdk/Exceptions/XunitException.cs @@ -0,0 +1,119 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; + +namespace Xunit.Sdk +{ + /// + /// The base assert exception class + /// +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + class XunitException : Exception, IAssertionException + { +#if XUNIT_NULLABLE + readonly string? stackTrace; +#else + readonly string stackTrace; +#endif + + /// + /// Initializes a new instance of the class. + /// + public XunitException() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The user message to be displayed +#if XUNIT_NULLABLE + public XunitException(string? userMessage) : + this(userMessage, (Exception?)null) +#else + public XunitException(string userMessage) : + this(userMessage, (Exception)null) +#endif + { } + + /// + /// 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) + { + UserMessage = userMessage; + } + + /// + /// Initializes a new instance of the class. + /// + /// The user message to be displayed + /// The stack trace to be displayed + protected XunitException( +#if XUNIT_NULLABLE + string? userMessage, + string? stackTrace) : +#else + string userMessage, + string stackTrace) : +#endif + this(userMessage) + { + this.stackTrace = stackTrace; + } + + /// + /// Gets a string representation of the frames on the call stack at the time the current exception was thrown. + /// + /// A string that describes the contents of the call stack, with the most recent method call appearing first. +#if XUNIT_NULLABLE + public override string? StackTrace => +#else + public override string StackTrace => +#endif + stackTrace ?? base.StackTrace; + + /// + /// Gets the user message + /// +#if XUNIT_NULLABLE + public string? UserMessage { get; protected set; } +#else + public string UserMessage { get; protected set; } +#endif + + /// + public override string ToString() + { + var className = GetType().ToString(); + var message = Message; + string result; + + if (message == null || message.Length <= 0) + result = className; + else + result = $"{className}: {message}"; + + var stackTrace = StackTrace; + if (stackTrace != null) + result = $"{result}{Environment.NewLine}{stackTrace}"; + + return result; + } + } +} diff --git a/Sdk/IAssertionException.cs b/Sdk/IAssertionException.cs new file mode 100644 index 0000000000000..1447009e109e0 --- /dev/null +++ b/Sdk/IAssertionException.cs @@ -0,0 +1,13 @@ +#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 . + /// + public interface IAssertionException + { } +} diff --git a/SetAsserts.cs b/SetAsserts.cs new file mode 100644 index 0000000000000..56c00ea84c73e --- /dev/null +++ b/SetAsserts.cs @@ -0,0 +1,231 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System.Collections.Generic; +using Xunit.Sdk; + +#if XUNIT_IMMUTABLE_COLLECTIONS +using System.Collections.Immutable; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 new ContainsException(expected, set); + } + +#if NET5_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 new ContainsException(expected, set); + } +#endif + + /// + /// 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); + +#if XUNIT_IMMUTABLE_COLLECTIONS + /// + /// 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); +#endif + + /// + /// 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 new DoesNotContainException(expected, set); + } + +#if NET5_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 container + public static void DoesNotContain( + T expected, + IReadOnlySet set) + { + GuardArgumentNotNull(nameof(set), set); + + if (set.Contains(expected)) + throw new DoesNotContainException(expected, set); + } +#endif + + /// + /// 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); + +#if XUNIT_IMMUTABLE_COLLECTIONS + /// + /// 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); +#endif + + /// + /// Verifies that a set is a proper subset of another set. + /// + /// The type of the object to be verified + /// The expected superset + /// 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 expectedSuperset, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expectedSuperset), expectedSuperset); + + if (actual == null || !actual.IsProperSubsetOf(expectedSuperset)) + throw new ProperSubsetException(expectedSuperset, actual); + } + + /// + /// Verifies that a set is a proper superset of another set. + /// + /// The type of the object to be verified + /// The expected subset + /// 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 expectedSubset, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expectedSubset), expectedSubset); + + if (actual == null || !actual.IsProperSupersetOf(expectedSubset)) + throw new ProperSupersetException(expectedSubset, actual); + } + + /// + /// Verifies that a set is a subset of another set. + /// + /// The type of the object to be verified + /// The expected superset + /// 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 expectedSuperset, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expectedSuperset), expectedSuperset); + + if (actual == null || !actual.IsSubsetOf(expectedSuperset)) + throw new SubsetException(expectedSuperset, actual); + } + + /// + /// Verifies that a set is a superset of another set. + /// + /// The type of the object to be verified + /// The expected subset + /// 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 expectedSubset, +#if XUNIT_NULLABLE + ISet? actual) +#else + ISet actual) +#endif + { + GuardArgumentNotNull(nameof(expectedSubset), expectedSubset); + + if (actual == null || !actual.IsSupersetOf(expectedSubset)) + throw new SupersetException(expectedSubset, actual); + } + } +} diff --git a/SkipAsserts.cs b/SkipAsserts.cs new file mode 100644 index 0000000000000..89e5ed0855ce2 --- /dev/null +++ b/SkipAsserts.cs @@ -0,0 +1,79 @@ +#if XUNIT_SKIP + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 new SkipException(reason); + } + + /// + /// Will skip the current test unless evaluates to true. + /// + /// When true, the test will continue to run; when false, + /// 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 new SkipException(reason); + } + + /// + /// Will skip the current test when evaluates to true. + /// + /// When true, the test will be skipped; when false, + /// 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 new SkipException(reason); + } + } +} + +#endif diff --git a/SpanAsserts.cs b/SpanAsserts.cs new file mode 100644 index 0000000000000..945d290bfd5f0 --- /dev/null +++ b/SpanAsserts.cs @@ -0,0 +1,806 @@ +#if XUNIT_SPAN + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Globalization; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + // NOTE: ref struct types (Span, ReadOnlySpan) are not Nullable, and thus there is no XUNIT_NULLABLE usage currently in this class + // This also means that null spans are identical to empty spans, (both in essence point to a 0 sized array of whatever type) + + // NOTE: we could consider StartsWith and EndsWith and use the Span extension methods to check difference, but, the current + // Exceptions for StartsWith and EndsWith are only built for string types, so those would need a change (or new non-string versions created). + + // NOTE: there is an implicit conversion operator on Span to ReadOnlySpan - however, I have found that the compiler sometimes struggles + // with identifying the proper methods to use, thus I have overloaded quite a few of the assertions in terms of supplying both + // Span and ReadOnlySpan based methods + + /// + /// Verifies that a span contains a given sub-span, using the default comparison type. + /// + /// 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) => + Contains((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span contains a given sub-span, using the default comparison type. + /// + /// 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) => + Contains((ReadOnlySpan)expectedSubSpan, actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span contains a given sub-span, using the default comparison type. + /// + /// 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) => + Contains(expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span contains a given sub-span, using the default comparison type. + /// + /// 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) => + Contains(expectedSubSpan, actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span contains a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be in the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-span is not present inside the span + public static void Contains( + Span expectedSubSpan, + Span actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, comparisonType); + + /// + /// Verifies that a span contains a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be in the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-span is not present inside the span + public static void Contains( + Span expectedSubSpan, + ReadOnlySpan actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlySpan)expectedSubSpan, actualSpan, comparisonType); + + /// + /// Verifies that a span contains a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be in the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-span is not present inside the span + public static void Contains( + ReadOnlySpan expectedSubSpan, + Span actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains(expectedSubSpan, (ReadOnlySpan)actualSpan, comparisonType); + + /// + /// Verifies that a span contains a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be in the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-span is not present inside the span + public static void Contains( + ReadOnlySpan expectedSubSpan, + ReadOnlySpan actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (actualSpan.IndexOf(expectedSubSpan, comparisonType) < 0) + throw new ContainsException(expectedSubSpan.ToString(), actualSpan.ToString()); + } + + /// + /// 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 new ContainsException(expectedSubSpan.ToArray(), actualSpan.ToArray()); + } + + /// + /// Verifies that a span does not contain a given sub-span, using the default comparison type. + /// + /// 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) => + DoesNotContain((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span does not contain a given sub-span, using the default comparison type. + /// + /// 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) => + DoesNotContain((ReadOnlySpan)expectedSubSpan, actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span does not contain a given sub-span, using the default comparison type. + /// + /// 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) => + DoesNotContain(expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span does not contain a given sub-span, using the default comparison type. + /// + /// 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) => + DoesNotContain((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span does not contain a given sub-span, using the given comparison type. + /// + /// The sub-span expected not to be in the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-span is present inside the span + public static void DoesNotContain( + Span expectedSubSpan, + Span actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, comparisonType); + + /// + /// Verifies that a span does not contain a given sub-span, using the given comparison type. + /// + /// The sub-span expected not to be in the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-span is present inside the span + public static void DoesNotContain( + Span expectedSubSpan, + ReadOnlySpan actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlySpan)expectedSubSpan, actualSpan, comparisonType); + + /// + /// Verifies that a span does not contain a given sub-span, using the given comparison type. + /// + /// The sub-span expected not to be in the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-span is present inside the span + public static void DoesNotContain( + ReadOnlySpan expectedSubSpan, + Span actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain(expectedSubSpan, (ReadOnlySpan)actualSpan, comparisonType); + + /// + /// Verifies that a span does not contain a given sub-span, using the given comparison type. + /// + /// The sub-span expected not to be in the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-span is present inside the span + public static void DoesNotContain( + ReadOnlySpan expectedSubSpan, + ReadOnlySpan actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (actualSpan.IndexOf(expectedSubSpan, comparisonType) > -1) + throw new DoesNotContainException(expectedSubSpan.ToString(), actualSpan.ToString()); + } + + /// + /// 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 + { + if (actualSpan.IndexOf(expectedSubSpan) > -1) + throw new DoesNotContainException(expectedSubSpan.ToArray(), actualSpan.ToArray()); + } + + /// + /// Verifies that a span starts with a given sub-span, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-span expected to be at the start of the span + /// The span to be inspected + /// Thrown when the span does not start with the expected subspan + public static void StartsWith( + Span expectedStartSpan, + Span actualSpan) => + StartsWith((ReadOnlySpan)expectedStartSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span starts with a given sub-span, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-span expected to be at the start of the span + /// The span to be inspected + /// Thrown when the span does not start with the expected subspan + public static void StartsWith( + Span expectedStartSpan, + ReadOnlySpan actualSpan) => + StartsWith((ReadOnlySpan)expectedStartSpan, actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span starts with a given sub-span, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-span expected to be at the start of the span + /// The span to be inspected + /// Thrown when the span does not start with the expected subspan + public static void StartsWith( + ReadOnlySpan expectedStartSpan, + Span actualSpan) => + StartsWith(expectedStartSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span starts with a given sub-span, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-span expected to be at the start of the span + /// The span to be inspected + /// Thrown when the span does not start with the expected subspan + public static void StartsWith( + ReadOnlySpan expectedStartSpan, + ReadOnlySpan actualSpan) => + StartsWith(expectedStartSpan, actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span starts with a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be at the start of the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the span does not start with the expected subspan + public static void StartsWith( + Span expectedStartSpan, + Span actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlySpan)expectedStartSpan, (ReadOnlySpan)actualSpan, comparisonType); + + /// + /// Verifies that a span starts with a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be at the start of the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the span does not start with the expected subspan + public static void StartsWith( + Span expectedStartSpan, + ReadOnlySpan actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlySpan)expectedStartSpan, actualSpan, comparisonType); + + /// + /// Verifies that a span starts with a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be at the start of the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the span does not start with the expected subspan + public static void StartsWith( + ReadOnlySpan expectedStartSpan, + Span actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith(expectedStartSpan, (ReadOnlySpan)actualSpan, comparisonType); + + /// + /// Verifies that a span starts with a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be at the start of the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the span does not start with the expected subspan + public static void StartsWith( + ReadOnlySpan expectedStartSpan, + ReadOnlySpan actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (!actualSpan.StartsWith(expectedStartSpan, comparisonType)) + throw new StartsWithException(expectedStartSpan.ToString(), actualSpan.ToString()); + } + + /// + /// Verifies that a span ends with a given sub-span, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-span expected to be at the end of the span + /// The span to be inspected + /// Thrown when the span does not end with the expected subspan + public static void EndsWith( + Span expectedEndSpan, + Span actualSpan) => + EndsWith((ReadOnlySpan)expectedEndSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span ends with a given sub-span, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-span expected to be at the end of the span + /// The span to be inspected + /// Thrown when the span does not end with the expected subspan + public static void EndsWith( + Span expectedEndSpan, + ReadOnlySpan actualSpan) => + EndsWith((ReadOnlySpan)expectedEndSpan, actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span ends with a given sub-span, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-span expected to be at the end of the span + /// The span to be inspected + /// Thrown when the span does not end with the expected subspan + public static void EndsWith( + ReadOnlySpan expectedEndSpan, + Span actualSpan) => + EndsWith(expectedEndSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span ends with a given sub-span, using the default StringComparison.CurrentCulture comparison type. + /// + /// The sub-span expected to be at the end of the span + /// The span to be inspected + /// Thrown when the span does not end with the expected subspan + public static void EndsWith( + ReadOnlySpan expectedEndSpan, + ReadOnlySpan actualSpan) => + EndsWith(expectedEndSpan, actualSpan, StringComparison.CurrentCulture); + + /// + /// Verifies that a span ends with a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be at the end of the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the span does not end with the expected subspan + public static void EndsWith( + Span expectedEndSpan, + Span actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlySpan)expectedEndSpan, (ReadOnlySpan)actualSpan, comparisonType); + + /// + /// Verifies that a span ends with a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be at the end of the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the span does not end with the expected subspan + public static void EndsWith( + Span expectedEndSpan, + ReadOnlySpan actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlySpan)expectedEndSpan, actualSpan, comparisonType); + + /// + /// Verifies that a span ends with a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be at the end of the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the span does not end with the expected subspan + public static void EndsWith( + ReadOnlySpan expectedEndSpan, + Span actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith(expectedEndSpan, (ReadOnlySpan)actualSpan, comparisonType); + + /// + /// Verifies that a span ends with a given sub-span, using the given comparison type. + /// + /// The sub-span expected to be at the end of the span + /// The span to be inspected + /// The type of string comparison to perform + /// Thrown when the span does not end with the expected subspan + public static void EndsWith( + ReadOnlySpan expectedEndSpan, + ReadOnlySpan actualSpan, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (!actualSpan.EndsWith(expectedEndSpan, comparisonType)) + throw new EndsWithException(expectedEndSpan.ToString(), actualSpan.ToString()); + } + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equivalent. + public static void Equal( + Span expectedSpan, + Span actualSpan) => + Equal((ReadOnlySpan)expectedSpan, (ReadOnlySpan)actualSpan, false, false, false, false); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equivalent. + public static void Equal( + Span expectedSpan, + ReadOnlySpan actualSpan) => + Equal((ReadOnlySpan)expectedSpan, actualSpan, false, false, false, false); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equivalent. + public static void Equal( + ReadOnlySpan expectedSpan, + Span actualSpan) => + Equal(expectedSpan, (ReadOnlySpan)actualSpan, false, false, false, false); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equivalent. + public static void Equal( + ReadOnlySpan expectedSpan, + ReadOnlySpan actualSpan) => + Equal(expectedSpan, actualSpan, false, false, false, false); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to true, ignores all white space differences during comparison. + /// Thrown when the spans are not equivalent. + public static void Equal( + Span expectedSpan, + Span actualSpan, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal((ReadOnlySpan)expectedSpan, (ReadOnlySpan)actualSpan, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to true, ignores all white space differences during comparison. + /// Thrown when the spans are not equivalent. + public static void Equal( + Span expectedSpan, + ReadOnlySpan actualSpan, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal((ReadOnlySpan)expectedSpan, actualSpan, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to true, removes all whitespaces and tabs before comparing. + /// Thrown when the spans are not equivalent. + public static void Equal( + ReadOnlySpan expectedSpan, + Span actualSpan, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal(expectedSpan, (ReadOnlySpan)actualSpan, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to true, ignores all white space differences during comparison. + /// Thrown when the spans are not equivalent. + public static void Equal( + ReadOnlySpan expectedSpan, + ReadOnlySpan actualSpan, + 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 = expectedSpan.Length; + var actualLength = actualSpan.Length; + + // Block used to fix edge case of Equal("", " ") when ignoreAllWhiteSpace enabled. + if (ignoreAllWhiteSpace) + { + if (expectedLength == 0 && SkipWhitespace(actualSpan, 0) == actualLength) + return; + if (actualLength == 0 && SkipWhitespace(expectedSpan, 0) == expectedLength) + return; + } + + while (expectedIndex < expectedLength && actualIndex < actualLength) + { + var expectedChar = expectedSpan[expectedIndex]; + var actualChar = actualSpan[actualIndex]; + + if (ignoreLineEndingDifferences && IsLineEnding(expectedChar) && IsLineEnding(actualChar)) + { + expectedIndex = SkipLineEnding(expectedSpan, expectedIndex); + actualIndex = SkipLineEnding(actualSpan, actualIndex); + } + else if (ignoreAllWhiteSpace && (IsWhiteSpace(expectedChar) || IsWhiteSpace(actualChar))) + { + expectedIndex = SkipWhitespace(expectedSpan, expectedIndex); + actualIndex = SkipWhitespace(actualSpan, actualIndex); + } + else if (ignoreWhiteSpaceDifferences && IsWhiteSpace(expectedChar) && IsWhiteSpace(actualChar)) + { + expectedIndex = SkipWhitespace(expectedSpan, expectedIndex); + actualIndex = SkipWhitespace(actualSpan, actualIndex); + } + else + { + if (ignoreCase) + { + expectedChar = char.ToUpperInvariant(expectedChar); + actualChar = char.ToUpperInvariant(actualChar); + } + + if (expectedChar != actualChar) + break; + + expectedIndex++; + actualIndex++; + } + } + + if (expectedIndex < expectedLength || actualIndex < actualLength) + throw new EqualException(expectedSpan.ToString(), actualSpan.ToString(), expectedIndex, actualIndex); + } + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equivalent. + public static void Equal( + Span expectedSpan, + Span actualSpan) + where T : IEquatable => + Equal((ReadOnlySpan)expectedSpan, (ReadOnlySpan)actualSpan); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equivalent. + public static void Equal( + Span expectedSpan, + ReadOnlySpan actualSpan) + where T : IEquatable => + Equal((ReadOnlySpan)expectedSpan, actualSpan); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equivalent. + public static void Equal( + ReadOnlySpan expectedSpan, + Span actualSpan) + where T : IEquatable => + Equal(expectedSpan, (ReadOnlySpan)actualSpan); + + /// + /// Verifies that two spans are equivalent. + /// + /// The expected span value. + /// The actual span value. + /// Thrown when the spans are not equivalent. + public static void Equal( + ReadOnlySpan expectedSpan, + ReadOnlySpan actualSpan) + where T : IEquatable + { + if (!expectedSpan.SequenceEqual(actualSpan)) + Equal(expectedSpan.ToArray(), actualSpan.ToArray()); + } + + // ReadOnlySpan helper methods + + static bool IsLineEnding(char c) => + c == '\r' || c == '\n'; + + static bool IsWhiteSpace(char c) + { + const char mongolianVowelSeparator = '\u180E'; + const char zeroWidthSpace = '\u200B'; + const char zeroWidthNoBreakSpace = '\uFEFF'; + const char tabulation = '\u0009'; + + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + + return + unicodeCategory == UnicodeCategory.SpaceSeparator || + c == mongolianVowelSeparator || + c == zeroWidthSpace || + c == zeroWidthNoBreakSpace || + c == tabulation; + } + + 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 (IsWhiteSpace(value[index])) + index++; + else + return index; + } + + return index; + } + } +} + +#endif diff --git a/StringAsserts.cs b/StringAsserts.cs new file mode 100644 index 0000000000000..11ec34ff02e8d --- /dev/null +++ b/StringAsserts.cs @@ -0,0 +1,405 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Text.RegularExpressions; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 new ContainsException(expectedSubstring, actualString); + } + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string which is 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 which is 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 given 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 && actualString.IndexOf(expectedSubstring, comparisonType) >= 0) + throw new DoesNotContainException(expectedSubstring, 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 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 string, using the given comparison type. + /// + /// The 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 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 new StartsWithException(expectedStartString, actualString); + } + + /// + /// Verifies that a string ends with a given string, using the current culture. + /// + /// The 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 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 string, using the given comparison type. + /// + /// The 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 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 new EndsWithException(expectedEndString, actualString); + } + + /// + /// 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( + string expectedRegexPattern, +#if XUNIT_NULLABLE + string? actualString) +#else + string actualString) +#endif + { + GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern); + + if (actualString == null || !Regex.IsMatch(actualString, expectedRegexPattern)) + throw new MatchesException(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 new MatchesException(expectedRegex.ToString(), actualString); + } + + /// + /// 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( + string expectedRegexPattern, +#if XUNIT_NULLABLE + string? actualString) +#else + string actualString) +#endif + { + GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern); + + if (actualString != null && Regex.IsMatch(actualString, expectedRegexPattern)) + throw new DoesNotMatchException(expectedRegexPattern, actualString); + } + + /// + /// 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 && expectedRegex.IsMatch(actualString)) + throw new DoesNotMatchException(expectedRegex.ToString(), actualString); + } + + /// + /// 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); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to true, ignores all white space differences during comparison. + /// Thrown when the strings are not equivalent. + 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 XUNIT_SPAN + if (expected == null && actual == null) + return; + if (expected == null || actual == null) + throw new EqualException(expected, actual, -1, -1); + + Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); +#else + // Start out assuming the one of the values is null + int expectedIndex = -1; + int actualIndex = -1; + int expectedLength = 0; + int actualLength = 0; + + if (expected == null) + { + if (actual == null) + return; + } + else if (actual != null) + { + // Walk the string, keeping separate indices since we can skip variable amounts of + // data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences. + expectedIndex = 0; + actualIndex = 0; + expectedLength = expected.Length; + actualLength = actual.Length; + + // Block used to fix edge case of Equal("", " ") when ignoreAllWhiteSpace enabled. + if (ignoreAllWhiteSpace) + { + if (expectedLength == 0 && SkipWhitespace(actual, 0) == actualLength) + return; + if (actualLength == 0 && SkipWhitespace(expected, 0) == expectedLength) + return; + } + + while (expectedIndex < expectedLength && actualIndex < actualLength) + { + char expectedChar = expected[expectedIndex]; + char actualChar = actual[actualIndex]; + + if (ignoreLineEndingDifferences && IsLineEnding(expectedChar) && IsLineEnding(actualChar)) + { + expectedIndex = SkipLineEnding(expected, expectedIndex); + actualIndex = SkipLineEnding(actual, actualIndex); + } + else if (ignoreAllWhiteSpace && (IsWhiteSpace(expectedChar) || IsWhiteSpace(actualChar))) + { + expectedIndex = SkipWhitespace(expected, expectedIndex); + actualIndex = SkipWhitespace(actual, actualIndex); + } + else if (ignoreWhiteSpaceDifferences && IsWhiteSpace(expectedChar) && IsWhiteSpace(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) + throw new EqualException(expected, actual, expectedIndex, actualIndex); +#endif + } + +#if !XUNIT_SPAN + static bool IsLineEnding(char c) => + c == '\r' || c == '\n'; + + static bool IsWhiteSpace(char c) => + c == ' ' || c == '\t'; + + static int SkipLineEnding( + string value, + int index) + { + if (value[index] == '\r') + ++index; + if (index < value.Length && value[index] == '\n') + ++index; + + return index; + } + + static int SkipWhitespace( + string value, + int index) + { + while (index < value.Length) + { + switch (value[index]) + { + case ' ': + case '\t': + index++; + break; + + default: + return index; + } + } + + return index; + } +#endif + } +} diff --git a/TypeAsserts.cs b/TypeAsserts.cs new file mode 100644 index 0000000000000..1ed1e79c297b2 --- /dev/null +++ b/TypeAsserts.cs @@ -0,0 +1,144 @@ +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Reflection; +using Xunit.Sdk; + +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + 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 + { + IsAssignableFrom(typeof(T), @object); + 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.GetTypeInfo().IsAssignableFrom(@object.GetType().GetTypeInfo())) + throw new IsAssignableFromException(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 + IsNotType(typeof(T), @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 + public static void IsNotType( + Type expectedType, +#if XUNIT_NULLABLE + object? @object) +#else + object @object) +#endif + { + GuardArgumentNotNull(nameof(expectedType), expectedType); + + if (@object != null && expectedType.Equals(@object.GetType())) + throw new IsNotTypeException(expectedType, @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 + /// 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 + { + IsType(typeof(T), @object); + 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 + { + GuardArgumentNotNull(nameof(expectedType), expectedType); + + if (@object == null) + throw new IsTypeException(expectedType.FullName, null); + + var actualType = @object.GetType(); + if (expectedType != actualType) + { + var expectedTypeName = expectedType.FullName; + var actualTypeName = actualType.FullName; + + if (expectedTypeName == actualTypeName) + { + expectedTypeName += $" ({expectedType.GetTypeInfo().Assembly.GetName().FullName})"; + actualTypeName += $" ({actualType.GetTypeInfo().Assembly.GetName().FullName})"; + } + + throw new IsTypeException(expectedTypeName, actualTypeName); + } + } + } +} From 1733ac120c80c8864858825ceaf8887edf21e66c Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 25 Mar 2023 12:39:44 -0700 Subject: [PATCH 3/5] Add fork of xunit.assert, baseline warnings, and adopt in libraries --- eng/testing/xunit/xunit.props | 9 +++++++-- .../Common/tests/TestUtilities/TestUtilities.csproj | 6 +++++- src/tests/Common/xunit/Directory.Build.props | 4 ++++ src/tests/Common/xunit/Directory.Build.targets | 5 +++++ .../Common/xunit/assert.xunit/DXUnit.Assert.csproj | 12 ++++++++++++ 5 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/tests/Common/xunit/Directory.Build.props create mode 100644 src/tests/Common/xunit/Directory.Build.targets create mode 100644 src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj diff --git a/eng/testing/xunit/xunit.props b/eng/testing/xunit/xunit.props index 64286ff55ea10..99bc585a64efe 100644 --- a/eng/testing/xunit/xunit.props +++ b/eng/testing/xunit/xunit.props @@ -8,8 +8,13 @@ - - + + + + + + + diff --git a/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj b/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj index cbb1f4ed69be9..dba83e737475a 100644 --- a/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj +++ b/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj @@ -107,7 +107,11 @@ - + + + + + diff --git a/src/tests/Common/xunit/Directory.Build.props b/src/tests/Common/xunit/Directory.Build.props new file mode 100644 index 0000000000000..0ab55615ad183 --- /dev/null +++ b/src/tests/Common/xunit/Directory.Build.props @@ -0,0 +1,4 @@ + + + + diff --git a/src/tests/Common/xunit/Directory.Build.targets b/src/tests/Common/xunit/Directory.Build.targets new file mode 100644 index 0000000000000..9db89eda3113f --- /dev/null +++ b/src/tests/Common/xunit/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj b/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj new file mode 100644 index 0000000000000..47ca3b5f42337 --- /dev/null +++ b/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj @@ -0,0 +1,12 @@ + + + + Library + $(NetCoreAppMinimum) + $(DefineConstants);XUNIT_NULLABLE + + + $(NoWarn);SA1400;CA1852;CA1859;CA2007;SA1121;CA2249;CA1845;CA1822;CA1823;IDE0020;IDE0054;IDE0031;IDE0059;CA1510;CA1805;CA1825;IDE0036;IDE0074 + + + \ No newline at end of file From dc6e2472fa5dd695e1484cdefd22972561e53db3 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 25 Mar 2023 18:56:58 -0700 Subject: [PATCH 4/5] Make assert.xunit aot-safe --- .../Common/xunit/assert.xunit/Comparers.cs | 1 + .../xunit/assert.xunit/DXUnit.Assert.csproj | 6 +- .../assert.xunit/Sdk/ArgumentFormatter.cs | 6 + .../Sdk/AssertEqualityComparer.cs | 173 +----------------- .../xunit/assert.xunit/Sdk/AssertHelper.cs | 15 +- 5 files changed, 23 insertions(+), 178 deletions(-) diff --git a/src/tests/Common/xunit/assert.xunit/Comparers.cs b/src/tests/Common/xunit/assert.xunit/Comparers.cs index cfa736a1a7bdf..44af7eb2b82ad 100644 --- a/src/tests/Common/xunit/assert.xunit/Comparers.cs +++ b/src/tests/Common/xunit/assert.xunit/Comparers.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Xunit.Sdk; namespace Xunit diff --git a/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj b/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj index 47ca3b5f42337..4163f7a3e4c31 100644 --- a/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj +++ b/src/tests/Common/xunit/assert.xunit/DXUnit.Assert.csproj @@ -2,9 +2,13 @@ Library - $(NetCoreAppMinimum) + $(NetCoreAppCurrent);$(NetCoreAppMinimum) $(DefineConstants);XUNIT_NULLABLE + true + true + true + $(NoWarn);SA1400;CA1852;CA1859;CA2007;SA1121;CA2249;CA1845;CA1822;CA1823;IDE0020;IDE0054;IDE0031;IDE0059;CA1510;CA1805;CA1825;IDE0036;IDE0074 diff --git a/src/tests/Common/xunit/assert.xunit/Sdk/ArgumentFormatter.cs b/src/tests/Common/xunit/assert.xunit/Sdk/ArgumentFormatter.cs index b83dedca5b696..c2e72c4fcd669 100644 --- a/src/tests/Common/xunit/assert.xunit/Sdk/ArgumentFormatter.cs +++ b/src/tests/Common/xunit/assert.xunit/Sdk/ArgumentFormatter.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -225,6 +226,11 @@ static string Format( static string FormatComplexValue( object value, int depth, + [DynamicallyAccessedMembers( + DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.NonPublicProperties + | DynamicallyAccessedMemberTypes.PublicFields + | DynamicallyAccessedMemberTypes.NonPublicFields)] Type type) { if (depth == MAX_DEPTH) diff --git a/src/tests/Common/xunit/assert.xunit/Sdk/AssertEqualityComparer.cs b/src/tests/Common/xunit/assert.xunit/Sdk/AssertEqualityComparer.cs index d37bca9d3595a..f5c2ab7eac747 100644 --- a/src/tests/Common/xunit/assert.xunit/Sdk/AssertEqualityComparer.cs +++ b/src/tests/Common/xunit/assert.xunit/Sdk/AssertEqualityComparer.cs @@ -10,6 +10,7 @@ #if XUNIT_NULLABLE using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; #endif namespace Xunit.Sdk @@ -116,11 +117,6 @@ public bool Equals( if (dictionariesEqual.HasValue) return dictionariesEqual.GetValueOrDefault(); - // Sets? - var setsEqual = CheckIfSetsAreEqual(x, y, typeInfo); - if (setsEqual.HasValue) - return setsEqual.GetValueOrDefault(); - // Enumerable? var enumerablesEqual = CheckIfEnumerablesAreEqual(x, y, out mismatchIndex); if (enumerablesEqual.HasValue) @@ -150,48 +146,9 @@ public bool Equals( // Implements IStructuralEquatable? var structuralEquatable = x as IStructuralEquatable; - if (structuralEquatable != null && structuralEquatable.Equals(y, new TypeErasedEqualityComparer(innerComparerFactory()))) + if (structuralEquatable != null && structuralEquatable.Equals(y, EqualityComparer.Default)) return true; - // Implements IEquatable? - var iequatableY = typeof(IEquatable<>).MakeGenericType(y.GetType()).GetTypeInfo(); - if (iequatableY.IsAssignableFrom(x.GetType().GetTypeInfo())) - { - var equalsMethod = iequatableY.GetDeclaredMethod(nameof(IEquatable.Equals)); - if (equalsMethod == null) - return false; - -#if XUNIT_NULLABLE - return equalsMethod.Invoke(x, new object[] { y }) is true; -#else - return (bool)equalsMethod.Invoke(x, new object[] { y }); -#endif - } - - // Implements IComparable? - var icomparableY = typeof(IComparable<>).MakeGenericType(y.GetType()).GetTypeInfo(); - if (icomparableY.IsAssignableFrom(x.GetType().GetTypeInfo())) - { - var compareToMethod = icomparableY.GetDeclaredMethod(nameof(IComparable.CompareTo)); - if (compareToMethod == null) - return false; - - try - { -#if XUNIT_NULLABLE - return compareToMethod.Invoke(x, new object[] { y }) is 0; -#else - return (int)compareToMethod.Invoke(x, new object[] { y }) == 0; -#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. - } - } - // Last case, rely on object.Equals return object.Equals(x, y); } @@ -296,136 +253,10 @@ public bool Equals( return dictionaryYKeys.Count == 0; } -#if XUNIT_NULLABLE - static MethodInfo? s_compareTypedSetsMethod; -#else - static MethodInfo s_compareTypedSetsMethod; -#endif - - bool? CheckIfSetsAreEqual( -#if XUNIT_NULLABLE - [AllowNull] T x, - [AllowNull] T y, -#else - T x, - T y, -#endif - TypeInfo typeInfo) - { - if (!IsSet(typeInfo)) - return null; - - var enumX = x as IEnumerable; - var enumY = y as IEnumerable; - if (enumX == null || enumY == null) - return null; - - Type elementType; - if (typeof(T).GenericTypeArguments.Length != 1) - elementType = typeof(object); - else - elementType = typeof(T).GenericTypeArguments[0]; - - if (s_compareTypedSetsMethod == null) - { - s_compareTypedSetsMethod = GetType().GetTypeInfo().GetDeclaredMethod(nameof(CompareTypedSets)); - if (s_compareTypedSetsMethod == null) - return false; - } - - var method = s_compareTypedSetsMethod.MakeGenericMethod(new Type[] { elementType }); -#if XUNIT_NULLABLE - return method.Invoke(this, new object[] { enumX, enumY }) is true; -#else - return (bool)method.Invoke(this, new object[] { enumX, enumY }); -#endif - } - - bool CompareTypedSets( - IEnumerable enumX, - IEnumerable enumY) - { - var setX = new HashSet(enumX.Cast()); - var setY = new HashSet(enumY.Cast()); - - return setX.SetEquals(setY); - } - - bool IsSet(TypeInfo typeInfo) => - typeInfo - .ImplementedInterfaces - .Select(i => i.GetTypeInfo()) - .Where(ti => ti.IsGenericType) - .Select(ti => ti.GetGenericTypeDefinition()) - .Contains(typeof(ISet<>).GetGenericTypeDefinition()); - /// public int GetHashCode(T obj) { throw new NotImplementedException(); } - - private class TypeErasedEqualityComparer : IEqualityComparer - { - readonly IEqualityComparer innerComparer; - - public TypeErasedEqualityComparer(IEqualityComparer innerComparer) - { - this.innerComparer = innerComparer; - } - -#if XUNIT_NULLABLE - static MethodInfo? s_equalsMethod; -#else - static MethodInfo s_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. - Type objectType = x.GetType() == y.GetType() ? x.GetType() : typeof(object); - - // Lazily initialize and cache the EqualsGeneric method. - if (s_equalsMethod == null) - { - s_equalsMethod = typeof(TypeErasedEqualityComparer).GetTypeInfo().GetDeclaredMethod(nameof(EqualsGeneric)); - if (s_equalsMethod == null) - return false; - } - -#if XUNIT_NULLABLE - return s_equalsMethod.MakeGenericMethod(objectType).Invoke(this, new object[] { x, y }) is true; -#else - return (bool)s_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) - { - throw new NotImplementedException(); - } - } } } diff --git a/src/tests/Common/xunit/assert.xunit/Sdk/AssertHelper.cs b/src/tests/Common/xunit/assert.xunit/Sdk/AssertHelper.cs index 2c598670030f2..7a186c7b679ea 100644 --- a/src/tests/Common/xunit/assert.xunit/Sdk/AssertHelper.cs +++ b/src/tests/Common/xunit/assert.xunit/Sdk/AssertHelper.cs @@ -24,12 +24,15 @@ internal static class AssertHelper static ConcurrentDictionary>> gettersByType = new ConcurrentDictionary>>(); #endif -#if XUNIT_NULLABLE - static Dictionary> GetGettersForType(Type type) => -#else - static Dictionary> GetGettersForType(Type type) => -#endif - gettersByType.GetOrAdd(type, _type => + static Dictionary> GetGettersForType( + Type type) => + gettersByType.GetOrAdd(type, + ([DynamicallyAccessedMembers( + DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.NonPublicProperties + | DynamicallyAccessedMemberTypes.PublicFields + | DynamicallyAccessedMemberTypes.NonPublicFields)] + Type _type) => { var fieldGetters = _type From 3e40e2af181b7f545318401d7688669e1820b6f3 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sat, 25 Mar 2023 20:47:29 -0700 Subject: [PATCH 5/5] Remove redundent addition of xunit.assert --- .../tests/StreamConformanceTests/StreamConformanceTests.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libraries/Common/tests/StreamConformanceTests/StreamConformanceTests.csproj b/src/libraries/Common/tests/StreamConformanceTests/StreamConformanceTests.csproj index eebde98cad90e..40c5e28c18eac 100644 --- a/src/libraries/Common/tests/StreamConformanceTests/StreamConformanceTests.csproj +++ b/src/libraries/Common/tests/StreamConformanceTests/StreamConformanceTests.csproj @@ -16,8 +16,6 @@ - -