diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3bf927f9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,282 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Global settings +[*] +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_style = space +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_style = space +indent_size = 2 + +[*.{md,json}] +indent_style = space +indent_size = 4 + +[*.cs] +indent_style = space +indent_size = 4 +max_line_length = 130 + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +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_switch_labels = true +csharp_indent_labels = one_less_than_current + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# only use var when it's obvious what the variable type is +csharp_style_var_for_built_in_types = false:none +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion + +# use language keywords instead of BCL types +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:warning +dotnet_sort_system_directives_first = true +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:error + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_readonly_field = true:error + +# 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 = do_not_ignore +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 + +# FxCop Analyzers +dotnet_diagnostic.CA1030.severity = none +dotnet_diagnostic.CA1034.severity = none +dotnet_diagnostic.CA1062.severity = suggestion +dotnet_code_quality.CA1062.exclude_extension_method_this_parameter = true +dotnet_code_quality.exclude_extension_method_this_parameter = true +dotnet_code_quality.null_check_validation_methods = ThrowIfArgumentIsNull +dotnet_diagnostic.CA1304.severity = error +dotnet_diagnostic.CA1307.severity = error +dotnet_diagnostic.CA1308.severity = error +dotnet_diagnostic.CA1309.severity = error +dotnet_diagnostic.CA3075.severity = none +dotnet_diagnostic.CA5369.severity = none + +# Banned API Analyzers +dotnet_diagnostic.RS0030.severity = error + +# IDE0004: Remove unnecessary cast +dotnet_diagnostic.IDE0004.severity = error + +# IDE0005: Remove unnecessary usings/imports +dotnet_diagnostic.IDE0005.severity = suggestion + +# IDE0051: Remove unused private members (no reads or writes) +dotnet_diagnostic.IDE0051.severity = error + +# IDE0052: Remove unread private members (writes but no reads) +dotnet_diagnostic.IDE0052.severity = error + +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = error + +# CS1574: XML comment on 'construct' has syntactically incorrect cref attribute 'name' +dotnet_diagnostic.CS1574.severity = error + +# StyleCop +# SA1028: Code should not contain trailing whitespace +dotnet_diagnostic.SA1028.severity = suggestion +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none +# SA1116: The parameters should begin on the line after the declaration, whenever the parameter span across multiple lines +dotnet_diagnostic.SA1116.severity = none +# SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line. +dotnet_diagnostic.SA1117.severity = none +# SA1200: Using directive should appear within a namespace declaration +dotnet_diagnostic.SA1200.severity = none +# SA1124: Do not use regions +dotnet_diagnostic.SA1124.severity = none +# SA1201: A property should not follow a method +dotnet_diagnostic.SA1201.severity = none +# SA1202: 'public' members should come before 'private' members +dotnet_diagnostic.SA1202.severity = none +# SA1204: Static members should appear before non-static members +dotnet_diagnostic.SA1204.severity = none +# SA1312: variable should begin with lower-case letter +dotnet_diagnostic.SA1312.severity = suggestion # re-enable if using statements can be discarded +# SA1313: parameter should begin with lower-case letter +dotnet_diagnostic.SA1313.severity = suggestion # re-enable when parameters discards are available +# SA1316: Tuple element names should use correct casing +dotnet_diagnostic.SA1316.severity = none +# SA1402: File may only contain a single type. Handled better by AV1507 +dotnet_diagnostic.SA1402.severity = none +# SA1404: Code analysis suppression should have justification +dotnet_diagnostic.SA1404.severity = none +# SA1413: Use trailing comma in multi-line initializers +dotnet_diagnostic.SA1413.severity = none +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = suggestion +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = suggestion +# SA1611: The documentation for parameter is missing +dotnet_diagnostic.SA1611.severity = suggestion +# SA1612: The parameter documentation for parameter should be at position +dotnet_diagnostic.SA1612.severity = suggestion +# SA1614: Element parameter documentation should have text +dotnet_diagnostic.SA1614.severity = suggestion +# SA1615: Element return value should be documented +dotnet_diagnostic.SA1615.severity = none +# SA1616: Element return value documentation should have text +dotnet_diagnostic.SA1616.severity = suggestion +# SA1618: The documentation for type parameter is missing +dotnet_diagnostic.SA1618.severity = suggestion +# SA1623: The property's documentation summary text should begin with: 'Gets' +dotnet_diagnostic.SA1623.severity = none +# SA1629: Documentation text should end with a period +dotnet_diagnostic.SA1629.severity = none +# SA1633: The file header is missing or not located at the top of the file +dotnet_diagnostic.SA1633.severity = none +# SA1642: Constructor summary documentation should begin with standard text +dotnet_diagnostic.SA1642.severity = suggestion + +# CSharpGuidelines + +# Purpose: Argument for parameter calls nested method +# Rationale: Modern debuggers allow stepping into nested calls, so no need to avoid them. +dotnet_diagnostic.AV1580.severity = none + +dotnet_diagnostic.AV1561.max_parameter_count = 5 +# AV1008: Class should be non-static or its name should be suffixed with Extensions +dotnet_diagnostic.AV1008.severity = none +# AV1010: Type hides inherited member +dotnet_diagnostic.AV1010.severity = none +# AV1115: Member or local function contains the word 'and', which suggests doing multiple things +dotnet_diagnostic.AV1115.severity = suggestion +# AV1130: Return type in signature for Type should be a collection interface instead of a concrete type +dotnet_diagnostic.AV1130.severity = none +# AV1135: null is returned from method which has return type of string, collection or task +dotnet_diagnostic.AV1135.severity = none # re-enable if we can distinguish between string, collection and task +# AV1210: Catch a specific exception instead of Exception, SystemException or ApplicationException +dotnet_diagnostic.AV1210.severity = none +# AV1250: Evaluate LINQ query before returning it +dotnet_diagnostic.AV1250.severity = suggestion +# AV1500: Method 'CallerIdentifier.DetermineCallerIdentity()' contains 10 statements, which exceeds the maximum of 7 statements +dotnet_diagnostic.AV1500.severity = none +# AV1532: Loop statement contains nested loop +dotnet_diagnostic.AV1532.severity = suggestion +# AV1535: Missing block in case or default clause of switch statement +dotnet_diagnostic.AV1535.severity = none # re-enable if we can adjust the formatting to not indent the scope braces +# AV1537: If-else-if construct should end with an unconditional else clause +dotnet_diagnostic.AV1537.severity = suggestion +# AV1551: Method overload with the most parameters should be virtual +dotnet_diagnostic.AV1551.severity = none +# AV1555: Avoid using non-(nullable-)boolean named arguments +dotnet_diagnostic.AV1555.severity = suggestion +# AV1561: Method contains 5 parameters, which exceeds the maximum of 3 parameters +dotnet_diagnostic.AV1561.severity = suggestion +# AV1564: Parameter in public or internal member is of type bool or bool? +dotnet_diagnostic.AV1564.severity = suggestion +# AV1706: Parameter 'p' should have a more descriptive name +dotnet_diagnostic.AV1706.severity = suggestion +# AV1708: Type name contains term that should be avoided +dotnet_diagnostic.AV1708.severity = suggestion +# AV1710: Field contains the name of its containing type +dotnet_diagnostic.AV1710.severity = none +# AV2202: Replace call to Nullable.HasValue with null check +dotnet_diagnostic.AV2202.severity = none +# AV2305: Missing XML comment for internally visible type or member +dotnet_diagnostic.AV2305.severity = none +# AV2407: Region should be removed +dotnet_diagnostic.AV2407.severity = none + +# Meziantou.Analyzer + +# Purpose: Use string.Equals instead of Equals operator +# Rationale: Does not improve readability +dotnet_diagnostic.MA0006.severity = none + +# Purpose: Regular expressions should not be vulnerable to Denial of Service attacks +# Rationale: See https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0009.md +dotnet_diagnostic.MA0009.severity = suggestion + +# ReSharper/Rider +# Convert lambda expression to method group +resharper_convert_closure_to_method_group_highlighting=none diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..859710da --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,29 @@ +changelog: + exclude: + labels: + - changelog-ignore + authors: [ ] + categories: + - title: Breaking Changes + labels: + - "breaking change" + - title: New features + labels: + - "feature" + - title: Improvements + labels: + - "enhancement" + - "performance" + - title: Fixes + labels: + - "bug" + - "regression" + - title: Fixes (extensibility) + labels: + - "extensibility" + - title: Documentation + labels: + - "documentation" + - title: Others + labels: + - "*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..3888b957 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,47 @@ +name: build + +on: + pull_request: + paths-ignore: + - docs/** + push: + paths-ignore: + - docs/** + +jobs: + build: + + runs-on: windows-latest + + env: + DOTNET_NOLOGO: true + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 8.0.x + + - name: Run NUKE + run: ./build.ps1 + env: + BranchSpec: ${{ github.ref }} + BuildNumber: ${{ github.run_number }} + ApiKey: ${{ secrets.NUGETAPIKEY }} + + - name: coveralls + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + file: TestResults/reports/lcov.info + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + path: ./Artifacts/* diff --git a/.gitignore b/.gitignore index 10c48415..9d2bbcd9 100644 --- a/.gitignore +++ b/.gitignore @@ -165,9 +165,10 @@ Package/Lib *.zip # Cake related -Build/** -!Build/packages.config Tools/** # Rider .idea* + +# Approval Tests +*.received.txt diff --git a/.nuke/.gitignore b/.nuke/.gitignore new file mode 100644 index 00000000..abe251a7 --- /dev/null +++ b/.nuke/.gitignore @@ -0,0 +1 @@ +/temp \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 00000000..070dafd4 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "ApiChecks", + "CalculateNugetVersion", + "Clean", + "CodeCoverage", + "Compile", + "Pack", + "Push", + "Restore", + "UnitTests" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "ApiKey": { + "type": "string", + "description": "The key to push to Nuget", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 00000000..f1590309 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "FluentAssertions.Json.sln" +} \ No newline at end of file diff --git a/AcceptApiChanges.ps1 b/AcceptApiChanges.ps1 new file mode 100644 index 00000000..22a3b8a8 --- /dev/null +++ b/AcceptApiChanges.ps1 @@ -0,0 +1,18 @@ +## This script is related to approval tests that protect developers from unintentional changes to the public API +## If your change does change the API on purpose and you double-checked correctness of the changes you can use this script to change the "approved" state of the API +## Make sure that you run the approval tests before running this script, because the tests generate *.received.txt files. + +$ApprovalFiles = ".\Tests\Approval.Tests\ApprovedApi\FluentAssertions.Json\"; + +## Remove current "approved" API +Remove-Item "$ApprovalFiles\*.verified.txt" + +## Copy new API from .received.txt files to .verified.txt files +## Note that .received.txt files are ignored in git and are not part of the repository +Get-ChildItem -Path $ApprovalFiles -Filter "*received.txt" | ForEach-Object { + $NewName = $_.FullName -replace 'received.txt', 'verified.txt' + Copy-Item $_.FullName $NewName +} + +Remove-Item "$ApprovalFiles\*.received.txt" + diff --git a/AcceptApiChanges.sh b/AcceptApiChanges.sh new file mode 100644 index 00000000..6d4c66ed --- /dev/null +++ b/AcceptApiChanges.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +## This script is related to approval tests that protect developers from unintentional changes to the public API +## If your change does change the API on purpose and you double-checked correctness of the changes you can use this script to change the "approved" state of the API +## Make sure that you run the approval tests before running this script, because the tests generate *.received.txt files. + +find Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/ -type f -name "*received.txt" | perl -pe 'print $_; s/received/verified/' | xargs -n2 mv diff --git a/Build/.editorconfig b/Build/.editorconfig new file mode 100644 index 00000000..5641e0f6 --- /dev/null +++ b/Build/.editorconfig @@ -0,0 +1,16 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning + +dotnet_diagnostic.ide0044.severity = none +dotnet_diagnostic.ide0051.severity = none + +# ReSharper properties +resharper_place_field_attribute_on_same_line = false diff --git a/Build/Build.cs b/Build/Build.cs new file mode 100644 index 00000000..8a440f2b --- /dev/null +++ b/Build/Build.cs @@ -0,0 +1,184 @@ +using System; +using System.Linq; +using Nuke.Common; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.Execution; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitVersion; +using Nuke.Common.Tools.ReportGenerator; +using Nuke.Common.Tools.Xunit; +using Nuke.Common.Utilities.Collections; +using static Nuke.Common.Tools.DotNet.DotNetTasks; +using static Nuke.Common.Tools.ReportGenerator.ReportGeneratorTasks; + +[UnsetVisualStudioEnvironmentVariables] +[DotNetVerbosityMapping] +class Build : NukeBuild +{ + /* Support plugins are available for: + - JetBrains ReSharper https://nuke.build/resharper + - JetBrains Rider https://nuke.build/rider + - Microsoft VisualStudio https://nuke.build/visualstudio + - Microsoft VSCode https://nuke.build/vscode + */ + public static int Main() => Execute(x => x.Push); + + GitHubActions GitHubActions => GitHubActions.Instance; + + string BranchSpec => GitHubActions?.Ref; + + string BuildNumber => GitHubActions?.RunNumber.ToString(); + + [Parameter("The key to push to Nuget")] + [Secret] + readonly string ApiKey; + + [Solution(GenerateProjects = true)] + readonly Solution Solution; + + [GitVersion(Framework = "net6.0")] + readonly GitVersion GitVersion; + + AbsolutePath SourceDirectory => RootDirectory / "src"; + + AbsolutePath TestsDirectory => RootDirectory / "tests"; + + AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; + + string SemVer; + + Target Clean => _ => _ + .Executes(() => + { + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(path => path.DeleteDirectory()); + TestsDirectory.GlobDirectories("**/bin", "**/obj").ForEach(path => path.DeleteDirectory()); + ArtifactsDirectory.CreateOrCleanDirectory(); + }); + + Target CalculateNugetVersion => _ => _ + .Executes(() => + { + SemVer = GitVersion.SemVer; + if (IsPullRequest) + { + Serilog.Log.Information( + "Branch spec {branchspec} is a pull request. Adding build number {buildnumber}", + BranchSpec, BuildNumber); + + SemVer = string.Join('.', GitVersion.SemVer.Split('.').Take(3).Union(new[] { BuildNumber })); + } + + Serilog.Log.Information("SemVer = {semver}", SemVer); + }); + + bool IsPullRequest => GitHubActions?.IsPullRequest ?? false; + + Target Restore => _ => _ + .DependsOn(Clean) + .Executes(() => + { + DotNetRestore(s => s + .SetProjectFile(Solution)); + }); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => + { + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration("CI") + .EnableNoLogo() + .EnableNoRestore()); + }); + + Target ApiChecks => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetTest(s => s + .SetConfiguration("Release") + .EnableNoBuild() + .CombineWith( + cc => cc.SetProjectFile(Solution.Approval_Tests))); + }); + + Target UnitTests => _ => _ + .DependsOn(Compile) + .Executes(() => + { + if (EnvironmentInfo.IsWin) + { + DotNetTest(s => s + .SetProjectFile(Solution.FluentAssertions_Json_Specs) + .SetFramework("net47") + .SetConfiguration("Debug") + .EnableNoBuild()); + } + + DotNetTest(s => s + .SetProjectFile(Solution.FluentAssertions_Json_Specs) + .SetFramework("net8.0") + .SetConfiguration("Debug") + .EnableNoBuild() + .SetDataCollector("XPlat Code Coverage") + .SetResultsDirectory(RootDirectory / "TestResults")); + }); + + Target CodeCoverage => _ => _ + .DependsOn(UnitTests) + .Executes(() => + { + ReportGenerator(s => s + .SetProcessToolPath(NuGetToolPathResolver.GetPackageExecutable("ReportGenerator", "ReportGenerator.dll", framework: "net6.0")) + .SetTargetDirectory(RootDirectory / "TestResults" / "reports") + .AddReports(RootDirectory / "TestResults/**/coverage.cobertura.xml") + .AddReportTypes("HtmlInline_AzurePipelines_Dark", "lcov") + .SetClassFilters("-System.Diagnostics.CodeAnalysis.StringSyntaxAttribute") + .SetAssemblyFilters("+FluentAssertions.Json")); + + string link = RootDirectory / "TestResults" / "reports" / "index.html"; + + Serilog.Log.Information($"Code coverage report: \x1b]8;;file://{link.Replace('\\', '/')}\x1b\\{link}\x1b]8;;\x1b\\"); + }); + + Target Pack => _ => _ + .DependsOn(ApiChecks) + .DependsOn(UnitTests) + .DependsOn(CodeCoverage) + .DependsOn(CalculateNugetVersion) + .Executes(() => + { + DotNetPack(s => s + .SetProject(Solution.FluentAssertions_Json) + .SetOutputDirectory(ArtifactsDirectory) + .SetConfiguration("Release") + .EnableNoLogo() + .EnableNoRestore() + .EnableContinuousIntegrationBuild() // Necessary for deterministic builds + .SetVersion(SemVer)); + }); + + Target Push => _ => _ + .DependsOn(Pack) + .OnlyWhenDynamic(() => IsTag) + .Executes(() => + { + var packages = ArtifactsDirectory.GlobFiles("*.nupkg"); + + Assert.NotEmpty(packages); + + DotNetNuGetPush(s => s + .SetApiKey(ApiKey) + .EnableSkipDuplicate() + .SetSource("https://api.nuget.org/v3/index.json") + .EnableNoSymbols() + .CombineWith(packages, + (v, path) => v.SetTargetPath(path))); + }); + + bool IsTag => BranchSpec != null && BranchSpec.Contains("refs/tags", StringComparison.OrdinalIgnoreCase); +} diff --git a/Build/_build.csproj b/Build/_build.csproj new file mode 100644 index 00000000..7bd6a66a --- /dev/null +++ b/Build/_build.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + + CS0649;CS0169 + ..\ + ..\ + + + + + + + + + + diff --git a/Build/_build.csproj.DotSettings b/Build/_build.csproj.DotSettings new file mode 100644 index 00000000..03060223 --- /dev/null +++ b/Build/_build.csproj.DotSettings @@ -0,0 +1,30 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + True + True + True + True + True + True + True + True + True + True diff --git a/Build/packages.config b/Build/packages.config deleted file mode 100644 index ba021c53..00000000 --- a/Build/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb37cffe..b8a14f8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ -Contributing to Fluent Assertions ------------ +# Contributing to Fluent Assertions -No open-source projection is going to be successfull without contributions. After we decided to move to Github, the involvement of the .NET community has increased significantly. However, contributing to this project involves a few steps that will seriously increase the change we will accept it. +No open-source project is going to be successful without contributions. After we decided to move to Github, the involvement of the .NET community has increased significantly. However, contributing to this project involves a few steps that will seriously increase the chance we will accept it. * The [Pull Request](https://help.github.com/articles/using-pull-requests) is targeted at the `master` branch. -* The code complies with the [Coding Guidelines for C# 3.0, 4.0 and 5.0](http://csharpguidelines.codeplex.com/)/. -* The changes are covered by a new or existing set of unit tests which follow the Arrange-Act-Assert syntax such as is used [in this example](https://github.com/dennisdoomen/fluentassertions/blob/daaf35b9b59b622c96d0c034e8972a020b2bee55/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs#L33). -* If the contribution affects the documentation, please help us by providing a paragraph of text and/or examples that we can include in the [documentation](https://github.com/dennisdoomen/fluentassertions/wiki) wiki. +* The code complies with the [Coding Guidelines for C#](https://csharpcodingguidelines.com/). +* The changes are covered by a new or existing set of unit tests which follow the Arrange-Act-Assert syntax such as is used [in this example](https://github.com/fluentassertions/fluentassertions/blob/daaf35b9b59b622c96d0c034e8972a020b2bee55/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs#L33). +* If the contribution changes the public API, the changes needs to be included by running [`AcceptApiChanges.ps1`](https://github.com/fluentassertions/fluentassertions.json/tree/master/AcceptApiChanges.ps1)/[`AcceptApiChanges.sh`](https://github.com/fluentassertions/fluentassertions/tree/master/AcceptApiChanges.sh) or using Rider's [Verify Support](https://plugins.jetbrains.com/plugin/17240-verify-support) plug-in. +* If the contribution affects the documentation, please update the [**readme.md**](https://github.com/fluentassertions/fluentassertions.json/tree/master/readme.md). diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..d06bc9d5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,40 @@ + + + 13.0 + true + + + + false + false + false + + + true + 9.0 + All + true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/FluentAssertions.Json.sln b/FluentAssertions.Json.sln index 207a8850..daaa4852 100644 --- a/FluentAssertions.Json.sln +++ b/FluentAssertions.Json.sln @@ -1,79 +1,50 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E5A0B454-22D4-4694-99FF-D6A8B7DE7DA3}" ProjectSection(SolutionItems) = preProject Tests\Default.testsettings = Tests\Default.testsettings - Src\FluentAssertions.nuspec = Src\FluentAssertions.nuspec README.md = README.md EndProjectSection EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Json.Shared.Specs", "Tests\FluentAssertions.Json.Shared.Specs\Json.Shared.Specs.shproj", "{845ED06B-1F6D-4442-BCCC-52483561F447}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Json", "Src\FluentAssertions.Json\FluentAssertions.Json.csproj", "{C1D6C0EB-A488-4166-A313-F024C5B6DF5A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Json.Net45.Specs", "Tests\FluentAssertions.Json.Net45.Specs\Json.Net45.Specs.csproj", "{63A1554C-DB57-463A-9DF4-2748EB403DD9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{27A3107F-A317-4538-8095-A0B55470C852}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Json", "Src\FluentAssertions.Json\FluentAssertions.Json.csproj", "{C1D6C0EB-A488-4166-A313-F024C5B6DF5A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Json.Specs", "Tests\FluentAssertions.Json.Specs\FluentAssertions.Json.Specs.csproj", "{3BC30A51-52A0-4F87-A962-D3473F9FD323}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Approval.Tests", "Tests\Approval.Tests\Approval.Tests.csproj", "{DB02EE93-8D2F-4094-9999-32D31F053BE2}" EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - Tests\FluentAssertions.Json.Shared.Specs\Shared.Json.Specs.projitems*{63a1554c-db57-463a-9df4-2748eb403dd9}*SharedItemsImports = 4 - Tests\FluentAssertions.Json.Shared.Specs\Shared.Json.Specs.projitems*{845ed06b-1f6d-4442-bccc-52483561f447}*SharedItemsImports = 13 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution + CI|Any CPU = CI|Any CPU Debug|Any CPU = Debug|Any CPU - Debug|ARM = Debug|ARM - Debug|Mixed Platforms = Debug|Mixed Platforms - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|ARM = Release|ARM - Release|Mixed Platforms = Release|Mixed Platforms - Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|ARM.ActiveCfg = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|ARM.Build.0 = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|x64.ActiveCfg = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|x64.Build.0 = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|x86.ActiveCfg = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Debug|x86.Build.0 = Debug|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|Any CPU.Build.0 = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|ARM.ActiveCfg = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|ARM.Build.0 = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|x64.ActiveCfg = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|x64.Build.0 = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|x86.ActiveCfg = Release|Any CPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9}.Release|x86.Build.0 = Release|Any CPU + {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.CI|Any CPU.ActiveCfg = Release|Any CPU + {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.CI|Any CPU.Build.0 = Release|Any CPU {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|ARM.ActiveCfg = Debug|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|ARM.Build.0 = Debug|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|x64.ActiveCfg = Debug|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|x64.Build.0 = Debug|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|x86.ActiveCfg = Debug|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Debug|x86.Build.0 = Debug|Any CPU {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|Any CPU.ActiveCfg = Release|Any CPU {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|Any CPU.Build.0 = Release|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|ARM.ActiveCfg = Release|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|ARM.Build.0 = Release|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|x64.ActiveCfg = Release|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|x64.Build.0 = Release|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|x86.ActiveCfg = Release|Any CPU - {C1D6C0EB-A488-4166-A313-F024C5B6DF5A}.Release|x86.Build.0 = Release|Any CPU + {27A3107F-A317-4538-8095-A0B55470C852}.CI|Any CPU.ActiveCfg = Release|Any CPU + {27A3107F-A317-4538-8095-A0B55470C852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27A3107F-A317-4538-8095-A0B55470C852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BC30A51-52A0-4F87-A962-D3473F9FD323}.CI|Any CPU.ActiveCfg = Debug|Any CPU + {3BC30A51-52A0-4F87-A962-D3473F9FD323}.CI|Any CPU.Build.0 = Debug|Any CPU + {3BC30A51-52A0-4F87-A962-D3473F9FD323}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BC30A51-52A0-4F87-A962-D3473F9FD323}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BC30A51-52A0-4F87-A962-D3473F9FD323}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BC30A51-52A0-4F87-A962-D3473F9FD323}.Release|Any CPU.Build.0 = Release|Any CPU + {DB02EE93-8D2F-4094-9999-32D31F053BE2}.CI|Any CPU.ActiveCfg = Release|Any CPU + {DB02EE93-8D2F-4094-9999-32D31F053BE2}.CI|Any CPU.Build.0 = Release|Any CPU + {DB02EE93-8D2F-4094-9999-32D31F053BE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB02EE93-8D2F-4094-9999-32D31F053BE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB02EE93-8D2F-4094-9999-32D31F053BE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB02EE93-8D2F-4094-9999-32D31F053BE2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FluentAssertions.Json.sln.DotSettings b/FluentAssertions.Json.sln.DotSettings index 1293109c..aaeb82d0 100644 --- a/FluentAssertions.Json.sln.DotSettings +++ b/FluentAssertions.Json.sln.DotSettings @@ -18,6 +18,8 @@ False <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> @@ -62,10 +64,12 @@ True True True + True True True True True + True D:\Workspaces\FluentAssertions\Default.testsettings 4 False diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 00000000..99fad930 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,13 @@ +next-version: 6.0 +branches: + release: + regex: releases?[/-] + tag: rc + pull-request: + mode: ContinuousDeployment + regex: ((pull|pull\-requests|pr)[/-]|[/-](merge)) + tag: pr + tag-number-pattern: '[/-]?(?\d+)' + prevent-increment-of-merged-branch-version: false +ignore: + sha: [] diff --git a/Lib/GitVersion.exe b/Lib/GitVersion.exe deleted file mode 100644 index 59a47f73..00000000 Binary files a/Lib/GitVersion.exe and /dev/null differ diff --git a/Lib/lib/win32/x64/git2-381caf5.dll b/Lib/lib/win32/x64/git2-381caf5.dll deleted file mode 100644 index af965f38..00000000 Binary files a/Lib/lib/win32/x64/git2-381caf5.dll and /dev/null differ diff --git a/Lib/lib/win32/x86/git2-381caf5.dll b/Lib/lib/win32/x86/git2-381caf5.dll deleted file mode 100644 index 37465eeb..00000000 Binary files a/Lib/lib/win32/x86/git2-381caf5.dll and /dev/null differ diff --git a/README.md b/README.md index d58312f5..68fec189 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ +[![build](https://github.com/fluentassertions/fluentassertions.json/actions/workflows/build.yml/badge.svg)](https://github.com/fluentassertions/fluentassertions.json/actions/workflows/build.yml) +[![](https://img.shields.io/github/release/FluentAssertions/FluentAssertions.Json.svg?label=latest%20release)](https://github.com/FluentAssertions/FluentAssertions.Json/releases/latest) +[![](https://img.shields.io/nuget/dt/FluentAssertions.Json.svg?label=nuget%20downloads)](https://www.nuget.org/packages/FluentAssertions.Json) +[![](https://img.shields.io/librariesio/dependents/nuget/FluentAssertions.Json.svg?label=dependent%20libraries)](https://libraries.io/nuget/FluentAssertions.Json) +![](https://img.shields.io/badge/release%20strategy-githubflow-orange.svg) +[![Coverage Status](https://coveralls.io/repos/github/fluentassertions/fluentassertions.json/badge.svg?branch=master)](https://coveralls.io/github/fluentassertions/fluentassertions.json?branch=master) + ## *"With Fluent Assertions, the assertions look beautiful, natural and most importantly, extremely readable"* (by [Girish](https://twitter.com/girishracharya)) * See [www.fluentassertions.com](http://www.fluentassertions.com/) for more information about the main library. -* The build status is [![Build status](https://ci.appveyor.com/api/projects/status/ub0dfcmad2cf26tf/branch/master?svg=true)](https://ci.appveyor.com/project/dennisdoomen/fluentassertions-json/branch/master) * Join the chat at [![Join the chat at https://gitter.im/dennisdoomen/fluentassertions](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dennisdoomen/fluentassertions?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ### Available extension methods @@ -35,3 +41,37 @@ var expected = JToken.Parse(@"{ ""key2"" : ""value"" }"); actual.Should().BeEquivalentTo(expected); ``` +You can also use `IJsonAssertionOptions<>` with `Should().BeEquivalentTo()` assertions, which contains helper methods that you can use to specify the way you want to compare specific data types. + +Example: + +```c# +using FluentAssertions; +using FluentAssertions.Json; +using Newtonsoft.Json.Linq; + +... +var actual = JToken.Parse(@"{ ""value"" : 1.5 }"); +var expected = JToken.Parse(@"{ ""value"" : 1.4 }"); +actual.Should().BeEquivalentTo(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 0.1)) + .WhenTypeIs()); +``` + +Also, there is `WithoutStrictOrdering()` which allows you to compare JSON arrays while ignoring the order of their elements. +This is useful when the sequence of items is not important for your test scenario. When applied, assertions like `BeEquivalentTo()` will +succeed as long as the arrays contain the same elements, regardless of their order. + +Example: + +```c# +using FluentAssertions; +using FluentAssertions.Json; +using Newtonsoft.Json.Linq; + +... +var actual = JToken.Parse(@"{ ""array"" : [1, 2, 3] }"); +var expected = JToken.Parse(@"{ ""array"" : [3, 2, 1] }"); +actual.Should().BeEquivalentTo(expected, options => options + .WithoutStrictOrdering()); +``` diff --git a/Src/FluentAssertions.Json/AssemblyInfo.cs b/Src/FluentAssertions.Json/AssemblyInfo.cs deleted file mode 100644 index ff79ae44..00000000 --- a/Src/FluentAssertions.Json/AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("FluentAssertions.Json")] -[assembly: AssemblyDescription("Fluent Assertions extensions for Newtonsoft.Json")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("FluentAssertions.Json")] - -[assembly: AssemblyCompany("www.continuousimprover.com")] -[assembly: AssemblyCopyright("Copyright Dennis Doomen 2010-2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -[assembly: AssemblyVersion("4.20.0.0")] -[assembly: AssemblyInformationalVersion("4.20.0+Branch.release-5.0.Sha.0619a4b0d933d605ba1f6f5c2a883f516ba9985e")] -[assembly: AssemblyFileVersion("4.20.0.0")] - diff --git a/Src/FluentAssertions.Json/BannedSymbols.txt b/Src/FluentAssertions.Json/BannedSymbols.txt new file mode 100644 index 00000000..2c79476c --- /dev/null +++ b/Src/FluentAssertions.Json/BannedSymbols.txt @@ -0,0 +1,5 @@ +F:System.StringComparison.CurrentCulture;Use Ordinal instead +F:System.StringComparison.CurrentCultureIgnoreCase;Use OrdinalIgnoreCase instead +F:System.StringComparison.InvariantCulture;Use Ordinal instead +F:System.StringComparison.InvariantCultureIgnoreCase;Use OrdinalIgnoreCase instead +P:System.Globalization.CultureInfo.CurrentCulture;Use InvariantCulture instead diff --git a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs new file mode 100644 index 00000000..7322bfce --- /dev/null +++ b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace FluentAssertions.Json.Common; + +internal static class JTokenExtensions +{ + private static readonly JTokenComparer Comparer = new(); + + /// + /// Recursively sorts the properties of JObject instances by name and + /// the elements of JArray instances by their string representation, + /// producing a normalized JToken for consistent comparison + /// + public static JToken Normalize(this JToken token) + { + return token switch + { + JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name, StringComparer.Ordinal).Select(p => new JProperty(p.Name, Normalize(p.Value)))), + JArray array => new JArray(array.Select(Normalize).OrderBy(x => x, Comparer)), + _ => token + }; + } + + private sealed class JTokenComparer : IComparer + { + public int Compare(JToken x, JToken y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + var typeComparison = x.Type.CompareTo(y.Type); + if (typeComparison != 0) + { + return typeComparison; + } + + return x switch + { + JArray a => Compare(a, (JArray)y), + JObject o => Compare(o, (JObject)y), + JProperty p => Compare(p, (JProperty)y), + JValue v => Compare(v, (JValue)y), + JConstructor c => Compare(c, (JConstructor)y), + _ => string.CompareOrdinal(x.ToString(), y.ToString()) + }; + } + + private static int Compare(JValue x, JValue y) => Comparer.Default.Compare(x.Value, y.Value); + + private static int Compare(JConstructor x, JConstructor y) + { + var nameComparison = string.CompareOrdinal(x.Name, y.Name); + return nameComparison != 0 ? nameComparison : Compare(x, (JContainer)y); + } + + private static int Compare(JContainer x, JContainer y) + { + var countComparison = x.Count.CompareTo(y.Count); + if (countComparison != 0) + { + return countComparison; + } + + return x + .Select((t, i) => Comparer.Compare(t, y[i])) + .FirstOrDefault(itemComparison => itemComparison != 0); + } + + private static int Compare(JObject x, JObject y) + { + var countComparison = x.Count.CompareTo(y.Count); + if (countComparison != 0) + { + return countComparison; + } + + return x.Properties() + .OrderBy(p => p.Name, StringComparer.Ordinal) + .Zip(y.Properties().OrderBy(p => p.Name, StringComparer.Ordinal), (px, py) => Compare(px, py)) + .FirstOrDefault(itemComparison => itemComparison != 0); + } + + private static int Compare(JProperty x, JProperty y) + { + var nameComparison = string.CompareOrdinal(x.Name, y.Name); + return nameComparison != 0 ? nameComparison : Comparer.Compare(x.Value, y.Value); + } + } +} diff --git a/Src/FluentAssertions.Json/Common/StringExtensions.cs b/Src/FluentAssertions.Json/Common/StringExtensions.cs index 38a8c872..928e6c35 100644 --- a/Src/FluentAssertions.Json/Common/StringExtensions.cs +++ b/Src/FluentAssertions.Json/Common/StringExtensions.cs @@ -1,86 +1,15 @@ -using System; -using System.Linq; -using FluentAssertions.Formatting; +namespace FluentAssertions.Json.Common; -namespace FluentAssertions.Json.Common +internal static class StringExtensions { - internal static class StringExtensions - { - /// - /// Finds the first index at which the does not match the - /// string anymore, including the exact casing. - /// - public static int IndexOfFirstMismatch(this string value, string expected) - { - return IndexOfFirstMismatch(value, expected, StringComparison.CurrentCulture); - } - - /// - /// Finds the first index at which the does not match the - /// string anymore, accounting for the specified . - /// - public static int IndexOfFirstMismatch(this string value, string expected, StringComparison stringComparison) - { - for (int index = 0; index < value.Length; index++) - { - if ((index >= expected.Length) || !value[index].ToString().Equals(expected[index].ToString(), stringComparison)) - { - return index; - } - } - - return -1; - } - - /// - /// Gets the quoted three characters at the specified index of a string, including the index itself. - /// - public static string IndexedSegmentAt(this string value, int index) - { - int length = Math.Min(value.Length - index, 3); - - return $"{Formatter.ToString(value.Substring(index, length))} (index {index})".Replace("{", "{{").Replace("}", "}}"); - } + /// + /// Replaces all characters that might conflict with formatting placeholders with their escaped counterparts. + /// + public static string EscapePlaceholders(this string value) => + value.Replace("{", "{{").Replace("}", "}}"); - /// - /// Replaces all characters that might conflict with formatting placeholders and newlines with their escaped counterparts. - /// - public static string Escape(this string value, bool escapePlaceholders = false) - { - value = value.Replace("\"", "\\\"").Replace("\n", @"\n").Replace("\r", @"\r"); - if (escapePlaceholders) - { - value = value.Replace("{", "{{").Replace("}", "}}"); - } - - return value; - } - - /// - /// Joins a string with one or more other strings using a specified separator. - /// - /// - /// Any string that is empty (including the original string) is ignored. - /// - public static string Combine(this string @this, string other, string separator = ".") - { - var strings = new[] { @this }.Concat(new[] {other}).Where(s => s.Length > 0).ToArray(); - return string.Join(separator, strings); - } - - /// - /// Changes the first character of a string to uppercase. - /// - public static string Capitalize(this string @this) - { - char[] charArray = @this.ToCharArray(); - charArray[0] = char.ToUpper(charArray[0]); - return new string(charArray); - } - - public static string RemoveNewLines(this string @this) - { - return @this.Replace("\n", "").Replace("\r", "").Replace("\\r\\n", ""); - } + public static string RemoveNewLines(this string @this) + { + return @this.Replace("\n", string.Empty).Replace("\r", string.Empty).Replace("\\r\\n", string.Empty); } -} \ No newline at end of file +} diff --git a/Src/FluentAssertions.Json/Difference.cs b/Src/FluentAssertions.Json/Difference.cs new file mode 100644 index 00000000..9ad92388 --- /dev/null +++ b/Src/FluentAssertions.Json/Difference.cs @@ -0,0 +1,47 @@ +using System; + +namespace FluentAssertions.Json; + +internal sealed class Difference +{ + public Difference(DifferenceKind kind, JPath path, object actual, object expected) + : this(kind, path) + { + Actual = actual; + Expected = expected; + } + + public Difference(DifferenceKind kind, JPath path) + { + Kind = kind; + Path = path; + } + + private DifferenceKind Kind { get; } + + private JPath Path { get; } + + private object Actual { get; } + + private object Expected { get; } + + public override string ToString() + { + return Kind switch + { + DifferenceKind.ActualIsNull => "is null", + DifferenceKind.ExpectedIsNull => "is not null", + DifferenceKind.OtherType => $"has {Actual} instead of {Expected} at {Path}", + DifferenceKind.OtherName => $"has a different name at {Path}", + DifferenceKind.OtherValue => $"has a different value at {Path}", + DifferenceKind.DifferentLength => $"has {Actual} elements instead of {Expected} at {Path}", + DifferenceKind.ActualMissesProperty => $"misses property {Path}", + DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}", + DifferenceKind.ActualMissesElement => $"misses expected element {Path}", + DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order", +#pragma warning disable MA0015 // Specify the parameter name in ArgumentException + _ => throw new ArgumentOutOfRangeException(), +#pragma warning restore MA0015 // Specify the parameter name in ArgumentException + }; + } +} diff --git a/Src/FluentAssertions.Json/DifferenceKind.cs b/Src/FluentAssertions.Json/DifferenceKind.cs new file mode 100644 index 00000000..34387bc5 --- /dev/null +++ b/Src/FluentAssertions.Json/DifferenceKind.cs @@ -0,0 +1,15 @@ +namespace FluentAssertions.Json; + +internal enum DifferenceKind +{ + ActualIsNull, + ExpectedIsNull, + OtherType, + OtherName, + OtherValue, + DifferentLength, + ActualMissesProperty, + ExpectedMissesProperty, + ActualMissesElement, + WrongOrder +} diff --git a/Src/FluentAssertions.Json/FluentAssertions.Json.csproj b/Src/FluentAssertions.Json/FluentAssertions.Json.csproj index 3959bfa4..62182a39 100644 --- a/Src/FluentAssertions.Json/FluentAssertions.Json.csproj +++ b/Src/FluentAssertions.Json/FluentAssertions.Json.csproj @@ -1,18 +1,36 @@  - net451;netstandard1.4 + net47;netstandard2.0 true - 1701;1702;1705;1591;1574;1572;1573;419 + $(NoWarn);1701;1702;1705;1591;1574;1572;1573;419 False True ..\FluentAssertions.snk - false + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true - - false + + Dennis Doomen;Marcel Körtgen + Fluent Assertions extensions for Newtonsoft.Json + https://www.fluentassertions.com + https://github.com/fluentassertions/fluentassertions.json.git + git + Apache-2.0 + FluentAssertions.png + See https://github.com/fluentassertions/fluentassertions.json/releases/ + Copyright 2024-$([System.DateTime]::Now.ToString(yyyy)) Xceed Software Inc., all rights reserved - - + + + + + + + + + + \ No newline at end of file diff --git a/Src/FluentAssertions.Json/IJsonAssertionOptions.cs b/Src/FluentAssertions.Json/IJsonAssertionOptions.cs new file mode 100644 index 00000000..9a581f19 --- /dev/null +++ b/Src/FluentAssertions.Json/IJsonAssertionOptions.cs @@ -0,0 +1,26 @@ +using System; +using FluentAssertions.Equivalency; + +namespace FluentAssertions.Json; + +/// +/// Provides the run-time details of the class. +/// +public interface IJsonAssertionOptions +{ + /// + /// Overrides the comparison of subject and expectation to use provided + /// when the predicate is met. + /// + /// + /// The assertion to execute when the predicate is met. + /// +#pragma warning disable CA1716 // CA1716: Identifiers should not match keywords + IJsonAssertionRestriction Using(Action> action); +#pragma warning restore CA1716 + + /// + /// Configures the JSON assertion to ignore the order of elements in arrays or collections during comparison, allowing for equivalency checks regardless of element sequence. + /// + IJsonAssertionOptions WithoutStrictOrdering(); +} diff --git a/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs b/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs new file mode 100644 index 00000000..a79f4166 --- /dev/null +++ b/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs @@ -0,0 +1,14 @@ +namespace FluentAssertions.Json; + +/// +/// Defines additional overrides when used with +/// +public interface IJsonAssertionRestriction +{ + /// + /// Allows overriding the way structural equality is applied to (nested) objects of type + /// + /// + IJsonAssertionOptions WhenTypeIs() + where TMemberType : TMember; +} diff --git a/Src/FluentAssertions.Json/JPath.cs b/Src/FluentAssertions.Json/JPath.cs new file mode 100644 index 00000000..3baeba27 --- /dev/null +++ b/Src/FluentAssertions.Json/JPath.cs @@ -0,0 +1,31 @@ +namespace FluentAssertions.Json; + +internal sealed class JPath +{ + private readonly string[] nodes; + + public JPath() + { + nodes = ["$"]; + } + + private JPath(JPath existingPath, string extraNode) + { + nodes = [.. existingPath.nodes, extraNode]; + } + + public JPath AddProperty(string name) + { + return new JPath(this, $".{name}"); + } + + public JPath AddIndex(int index) + { + return new JPath(this, $"[{index}]"); + } + + public override string ToString() + { + return string.Concat(nodes); + } +} diff --git a/Src/FluentAssertions.Json/JTokenAssertions.cs b/Src/FluentAssertions.Json/JTokenAssertions.cs index db5edaf7..d45edcd2 100644 --- a/Src/FluentAssertions.Json/JTokenAssertions.cs +++ b/Src/FluentAssertions.Json/JTokenAssertions.cs @@ -1,505 +1,582 @@ -using System.Diagnostics; -using System.Linq; +using System; +using System.Diagnostics; +using System.Text.RegularExpressions; using FluentAssertions.Collections; -using FluentAssertions.Json.Common; using FluentAssertions.Execution; using FluentAssertions.Formatting; +using FluentAssertions.Json.Common; using FluentAssertions.Primitives; using Newtonsoft.Json.Linq; -using System.Text.RegularExpressions; -using System; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// Contains a number of methods to assert that an is in the expected state. +/// +[DebuggerNonUserCode] +public class JTokenAssertions : ReferenceTypeAssertions { + private GenericCollectionAssertions EnumerableSubject { get; } + + static JTokenAssertions() + { + Formatter.AddFormatter(new JTokenFormatter()); + } + /// - /// Contains a number of methods to assert that an is in the expected state. + /// Initializes a new instance of the class. /// - [DebuggerNonUserCode] - public class JTokenAssertions : ReferenceTypeAssertions + /// The subject + /// The assertion chain + public JTokenAssertions(JToken subject, AssertionChain assertionChain) + : base(subject, assertionChain) { - private GenericCollectionAssertions EnumerableSubject { get; } + EnumerableSubject = new GenericCollectionAssertions(subject, assertionChain); + } - static JTokenAssertions() - { - Formatter.AddFormatter(new JTokenFormatter()); - } + /// + /// Returns the type of the subject the assertion applies on. + /// + protected override string Identifier => nameof(JToken); - /// - /// Initializes a new instance of the class. - /// - /// The subject - public JTokenAssertions(JToken subject) - { - Subject = subject; - EnumerableSubject = new GenericCollectionAssertions(subject); - } + /// + /// Asserts that the current is equivalent to the parsed JSON, + /// using an equivalent of . + /// + /// The string representation of the expected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeEquivalentTo(string expected, string because = "", + params object[] becauseArgs) + { + JToken parsedExpected = Parse(expected, nameof(expected)); - /// - /// Returns the type of the subject the assertion applies on. - /// - protected override string Identifier => nameof(JToken); - - /// - /// Asserts that the current is equivalent to the parsed JSON, - /// using an equivalent of . - /// - /// The string representation of the expected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint BeEquivalentTo(string expected, string because = "", - params object[] becauseArgs) - { - JToken parsedExpected; - try - { - parsedExpected = JToken.Parse(expected); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse expected JSON string:{Environment.NewLine}" + - $"{expected}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(expected), ex); - } - - return BeEquivalentTo(parsedExpected, because, becauseArgs); - } + return BeEquivalentTo(parsedExpected, because, becauseArgs); + } - /// - /// Asserts that the current is equivalent to the element, - /// using an equivalent of . - /// - /// The expected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint BeEquivalentTo(JToken expected, string because = "", - params object[] becauseArgs) - { - Difference difference = JTokenDifferentiator.FindFirstDifference(Subject, expected); - - var message = $"JSON document {difference}.{Environment.NewLine}" + - $"Expected{Environment.NewLine}" + - $"{Format(Subject, true).Replace("{", "{{").Replace("}", "}}")}{Environment.NewLine}" + - $"to be equivalent to{Environment.NewLine}" + - $"{Format(expected, true).Replace("{", "{{").Replace("}", "}}")}{Environment.NewLine}" + - "{reason}."; - - Execute.Assertion - .ForCondition(difference == null) - .BecauseOf(because, becauseArgs) - .FailWith(message); + /// + /// Asserts that the current is equivalent to the element, + /// using an equivalent of . + /// + /// The expected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeEquivalentTo(JToken expected, string because = "", + params object[] becauseArgs) + { + return BeEquivalentTo(expected, false, options => options, because, becauseArgs); + } - return new AndConstraint(this); - } + /// + /// Asserts that the current is equivalent to the element, + /// using an equivalent of . + /// + /// The expected element + /// The options to consider while asserting values + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeEquivalentTo(JToken expected, + Func, IJsonAssertionOptions> config, + string because = "", + params object[] becauseArgs) + { + return BeEquivalentTo(expected, false, config, because, becauseArgs); + } - /// - /// Asserts that the current is not equivalent to the parsed JSON, - /// using an equivalent of . - /// - /// The string representation of the unexpected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint NotBeEquivalentTo(string unexpected, string because = "", - params object[] becauseArgs) - { - JToken parsedUnexpected; - try - { - parsedUnexpected = JToken.Parse(unexpected); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse unexpected JSON string:{Environment.NewLine}" + - $"{unexpected}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(unexpected), ex); - } - - return NotBeEquivalentTo(parsedUnexpected, because, becauseArgs); - } + private AndConstraint BeEquivalentTo(JToken expected, bool ignoreExtraProperties, + Func, IJsonAssertionOptions> config, + string because = "", + params object[] becauseArgs) + { + var differentiator = new JTokenDifferentiator(ignoreExtraProperties, config); + Difference difference = differentiator.FindFirstDifference(Subject, expected); - /// - /// Asserts that the current is not equivalent to the element, - /// using an equivalent of . - /// - /// The unexpected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint NotBeEquivalentTo(JToken unexpected, string because = "", params object[] becauseArgs) - { - Execute.Assertion - .ForCondition((ReferenceEquals(Subject, null) && !ReferenceEquals(unexpected, null)) || - !JToken.DeepEquals(Subject, unexpected)) - .BecauseOf(because, becauseArgs) - .FailWith("Expected JSON document not to be equivalent to {0}{reason}.", unexpected); + var expectation = ignoreExtraProperties ? "was expected to contain" : "was expected to be equivalent to"; - return new AndConstraint(this); - } + var message = $"JSON document {difference?.ToString().EscapePlaceholders()}.{Environment.NewLine}" + + $"Actual document{Environment.NewLine}" + + $"{Format(Subject, true).EscapePlaceholders()}{Environment.NewLine}" + + $"{expectation}{Environment.NewLine}" + + $"{Format(expected, true).EscapePlaceholders()}{Environment.NewLine}" + + "{reason}."; - /// - /// Asserts that the current has the specified value. - /// - /// The expected value - public AndConstraint HaveValue(string expected) - { - return HaveValue(expected, string.Empty); - } + CurrentAssertionChain + .ForCondition(difference == null) + .BecauseOf(because, becauseArgs) + .FailWith(message); - /// - /// Asserts that the current has the specified value. - /// - /// The expected value - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint HaveValue(string expected, string because, params object[] becauseArgs) - { - Execute.Assertion - .ForCondition(Subject.Value() == expected) - .BecauseOf(because, becauseArgs) - .FailWith("Expected JSON property {0} to have value {1}{reason}, but found {2}.", - Subject.Path, expected, Subject.Value()); + return new AndConstraint(this); + } - return new AndConstraint(this); - } + /// + /// Asserts that the current is not equivalent to the parsed JSON, + /// using an equivalent of . + /// + /// The string representation of the unexpected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotBeEquivalentTo(string unexpected, string because = "", + params object[] becauseArgs) + { + JToken parsedUnexpected = Parse(unexpected, nameof(unexpected)); - /// - /// Asserts that the current does not have the specified value. - /// - /// The unexpected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint NotHaveValue(string unexpected, string because = "", params object[] becauseArgs) - { - Execute.Assertion - .ForCondition(Subject.Value() != unexpected) - .BecauseOf(because, becauseArgs) - .FailWith("Did not expect JSON property {0} to have value {1}{reason}.", - Subject.Path, unexpected, Subject.Value()); + return NotBeEquivalentTo(parsedUnexpected, because, becauseArgs); + } - return new AndConstraint(this); - } + /// + /// Asserts that the current is not equivalent to the element, + /// using an equivalent of . + /// + /// The unexpected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotBeEquivalentTo(JToken unexpected, string because = "", params object[] becauseArgs) + { + CurrentAssertionChain + .ForCondition((Subject is null && unexpected is not null) || + !JToken.DeepEquals(Subject, unexpected)) + .BecauseOf(because, becauseArgs) + .FailWith("Expected JSON document not to be equivalent to {0}{reason}.", unexpected); - /// - /// Asserts that the current matches the specified regular expression pattern. - /// - /// The expected regular expression pattern - public AndConstraint MatchRegex(string regularExpression) - { - return MatchRegex(regularExpression, string.Empty); - } + return new AndConstraint(this); + } - /// - /// Asserts that the current matches the specified regular expression pattern. - /// - /// The expected regular expression pattern - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint MatchRegex(string regularExpression, string because, params object[] becauseArgs) - { - if (regularExpression == null) { - throw new ArgumentNullException(nameof(regularExpression), "MatchRegex does not support pattern"); - } + /// + /// Asserts that the current has the specified value. + /// + /// The expected value + public AndConstraint HaveValue(string expected) + { + return HaveValue(expected, string.Empty); + } - Execute.Assertion - .ForCondition(Regex.IsMatch(Subject.Value(), regularExpression)) - .BecauseOf(because, becauseArgs) - .FailWith("Expected {context:JSON property} {0} to match regex pattern {1}{reason}, but found {2}.", - Subject.Path, regularExpression, Subject.Value()); + /// + /// Asserts that the current has the specified value. + /// + /// The expected value + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint HaveValue(string expected, string because, params object[] becauseArgs) + { + CurrentAssertionChain + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected JSON token to have value {0}, but the element was .", expected); + + CurrentAssertionChain + .ForCondition(Subject.Value() == expected) + .BecauseOf(because, becauseArgs) + .FailWith("Expected JSON property {0} to have value {1}{reason}, but found {2}.", + Subject.Path, expected, Subject.Value()); + + return new AndConstraint(this); + } - return new AndConstraint(this); - } + /// + /// Asserts that the current does not have the specified value. + /// + /// The unexpected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotHaveValue(string unexpected, string because = "", params object[] becauseArgs) + { + CurrentAssertionChain + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect the JSON property to have value {0}, but the token was .", unexpected); + + CurrentAssertionChain + .ForCondition(Subject.Value() != unexpected) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect JSON property {0} to have value {1}{reason}.", + Subject.Path, unexpected, Subject.Value()); + + return new AndConstraint(this); + } - /// - /// Asserts that the current does not match the specified regular expression pattern. - /// - /// The expected regular expression pattern - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint NotMatchRegex(string regularExpression, string because = "", params object[] becauseArgs) + /// + /// Asserts that the current matches the specified regular expression pattern. + /// + /// The expected regular expression pattern + public AndConstraint MatchRegex(string regularExpression) + { + return MatchRegex(regularExpression, string.Empty); + } + + /// + /// Asserts that the current matches the specified regular expression pattern. + /// + /// The expected regular expression pattern + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint MatchRegex(string regularExpression, string because, params object[] becauseArgs) + { + if (regularExpression == null) { - if (regularExpression == null) { - throw new ArgumentNullException(nameof(regularExpression), "MatchRegex does not support pattern"); - } + throw new ArgumentNullException(nameof(regularExpression), "MatchRegex does not support pattern"); + } - Execute.Assertion - .ForCondition(!Regex.IsMatch(Subject.Value(), regularExpression)) - .BecauseOf(because, becauseArgs) - .FailWith("Did not expect {context:JSON property} {0} to match regex pattern {1}{reason}.", - Subject.Path, regularExpression, Subject.Value()); + CurrentAssertionChain + .ForCondition(Regex.IsMatch(Subject.Value(), regularExpression)) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:JSON property} {0} to match regex pattern {1}{reason}, but found {2}.", + Subject.Path, regularExpression, Subject.Value()); - return new AndConstraint(this); - } + return new AndConstraint(this); + } - /// - /// Asserts that the current has a direct child element with the specified - /// name. - /// - /// The name of the expected child element - public AndWhichConstraint HaveElement(string expected) + /// + /// Asserts that the current does not match the specified regular expression pattern. + /// + /// The expected regular expression pattern + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotMatchRegex(string regularExpression, string because = "", params object[] becauseArgs) + { + if (regularExpression == null) { - return HaveElement(expected, string.Empty); + throw new ArgumentNullException(nameof(regularExpression), "MatchRegex does not support pattern"); } - /// - /// Asserts that the current has a direct child element with the specified - /// name. - /// - /// The name of the expected child element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndWhichConstraint HaveElement(string expected, string because, - params object[] becauseArgs) - { - JToken jToken = Subject[expected]; - Execute.Assertion - .ForCondition(jToken != null) - .BecauseOf(because, becauseArgs) - .FailWith("Expected JSON document {0} to have element \"" + expected.Escape(true) + "\"{reason}" + - ", but no such element was found.", Subject); - - return new AndWhichConstraint(this, jToken); - } + CurrentAssertionChain + .ForCondition(!Regex.IsMatch(Subject.Value(), regularExpression)) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect {context:JSON property} {0} to match regex pattern {1}{reason}.", + Subject.Path, regularExpression, Subject.Value()); - /// - /// Asserts that the current does not have a direct child element with the specified - /// name. - /// - /// The name of the not expected child element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndWhichConstraint NotHaveElement(string unexpected, string because = "", - params object[] becauseArgs) - { - JToken jToken = Subject[unexpected]; - Execute.Assertion - .ForCondition(jToken == null) - .BecauseOf(because, becauseArgs) - .FailWith("Did not expect JSON document {0} to have element \"" + unexpected.Escape(true) + "\"{reason}.", Subject); + return new AndConstraint(this); + } - return new AndWhichConstraint(this, jToken); - } + /// + /// Asserts that the current has a direct child element with the specified + /// name. + /// + /// The name of the expected child element + public AndWhichConstraint HaveElement(string expected) + { + return HaveElement(expected, string.Empty); + } - /// - /// Expects the current to contain only a single item. - /// - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) - { - string formattedDocument = Format(Subject).Replace("{", "{{").Replace("}", "}}"); + /// + /// Asserts that the current has a direct child element with the specified + /// name. + /// + /// The name of the expected child element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint HaveElement(string expected, string because, + params object[] becauseArgs) + { + JToken jToken = Subject[expected]; - using (new AssertionScope("JSON document " + formattedDocument)) - { - var constraint = EnumerableSubject.ContainSingle(because, becauseArgs); - return new AndWhichConstraint(this, constraint.Which); - } - } + CurrentAssertionChain + .ForCondition(jToken != null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected JSON document {0} to have element \"" + expected.EscapePlaceholders() + "\"{reason}" + + ", but no such element was found.", Subject); - /// - /// Asserts that the number of items in the current matches the supplied amount. - /// - /// The expected number of items in the current . - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) - { - string formattedDocument = Format(Subject).Replace("{", "{{").Replace("}", "}}"); + return new AndWhichConstraint(this, jToken); + } - using (new AssertionScope("JSON document " + formattedDocument)) - { - EnumerableSubject.HaveCount(expected, because, becauseArgs); - return new AndConstraint(this); - } - } - - /// - /// Recursively asserts that the current contains at least the properties or elements of the specified . - /// - /// The subtree to search for - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - /// Use this method to match the current against an arbitrary subtree, - /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, - /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. - /// - /// This example asserts the values of multiple properties of a child object within a JSON document. - /// - /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }"); - /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }")); - /// - /// - /// This example asserts that a within a has at least one element with at least the given properties - /// - /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); - /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); - /// - public AndConstraint ContainSubtree(JToken subtree, string because = "", params object[] becauseArgs) + /// + /// Asserts that the current does not have a direct child element with the specified + /// name. + /// + /// The name of the not expected child element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint NotHaveElement(string unexpected, string because = "", + params object[] becauseArgs) + { + JToken jToken = Subject[unexpected]; + + CurrentAssertionChain + .ForCondition(jToken == null) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect JSON document {0} to have element \"" + unexpected.EscapePlaceholders() + "\"{reason}.", Subject); + + return new AndWhichConstraint(this, jToken); + } + + /// + /// Expects the current to contain only a single item. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) + { + string formattedDocument = Format(Subject).Replace("{", "{{").Replace("}", "}}"); + + using (new AssertionScope("JSON document " + formattedDocument)) { - Execute.Assertion - .ForCondition(JTokenContainsSubtree(Subject, subtree)) - .BecauseOf(because, becauseArgs) - .FailWith("Expected JSON document to contain subtree {0} {reason}, but some elements were missing.", subtree); // todo: report exact cause of failure, eg. name of the missing property, etc. + var constraint = EnumerableSubject.ContainSingle(because, becauseArgs); + return new AndWhichConstraint(this, constraint.Which); + } + } + + /// + /// Asserts that the number of items in the current matches the supplied amount. + /// + /// The expected number of items in the current . + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) + { + string formattedDocument = Format(Subject).Replace("{", "{{").Replace("}", "}}"); + using (new AssertionScope("JSON document " + formattedDocument)) + { + EnumerableSubject.HaveCount(expected, because, becauseArgs); return new AndConstraint(this); } + } + + /// + /// Recursively asserts that the current contains at least the properties or elements of the specified . + /// + /// The subtree to search for + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Use this method to match the current against an arbitrary subtree, + /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, + /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. + /// + /// This example asserts the values of multiple properties of a child object within a JSON document. + /// + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'John' } }"); + /// json.Should().ContainSubtree("{ success: true, data: { type: 'my-type', name: 'John' } }"); + /// + /// + /// + /// This example asserts that a within a has at least one element with at least the given properties + /// + /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); + /// json.Should().ContainSubtree("{ items: [ { type: 'my-type', name: 'Alpha' } ] }"); + /// + /// + public AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) + { + JToken subtreeToken = Parse(subtree, nameof(subtree)); + + return ContainSubtree(subtreeToken, because, becauseArgs); + } + + /// + /// Recursively asserts that the current contains at least the properties or elements of the specified . + /// + /// The subtree to search for + /// The options to consider while asserting values + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Use this method to match the current against an arbitrary subtree, + /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, + /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. + /// + /// This example asserts the values of multiple properties of a child object within a JSON document using a specified double precision. + /// + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', value: 0.99 } }"); + /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', value: 1.0 } }"), options => options + /// .Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1)) + /// .WhenTypeIs<double>()); + /// + /// + /// + /// This example asserts that a within a has at least one element with at least the given properties, using a specified double precision. + /// + /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', value: 0.99 }, { id: 3, type: 'other-type', value: 3 } ] }"); + /// json.Should().ContainSubtree("{ items: [ { type: 'my-type', value: 1 } ] }", options => options + /// .Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1)) + /// .WhenTypeIs<double>()); + /// + /// + public AndConstraint ContainSubtree(string subtree, + Func, IJsonAssertionOptions> config, + string because = "", + params object[] becauseArgs) + { + JToken subtreeToken = Parse(subtree, nameof(subtree)); - /// - /// Recursively asserts that the current contains at least the properties or elements of the specified . - /// - /// The subtree to search for - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - /// Use this method to match the current against an arbitrary subtree, - /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, - /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. - /// - /// This example asserts the values of multiple properties of a child object within a JSON document. - /// - /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }"); - /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }")); - /// - /// - /// This example asserts that a within a has at least one element with at least the given properties - /// - /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); - /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); - /// - public AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) + return BeEquivalentTo(subtreeToken, true, config, because, becauseArgs); + } + + /// + /// Recursively asserts that the current contains at least the properties or elements of the specified . + /// + /// The subtree to search for + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Use this method to match the current against an arbitrary subtree, + /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, + /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. + /// + /// This example asserts the values of multiple properties of a child object within a JSON document. + /// + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'John' } }"); + /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'John' } }")); + /// + /// + /// + /// This example asserts that a within a has at least one element with at least the given properties + /// + /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); + /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); + /// + /// + public AndConstraint ContainSubtree(JToken subtree, string because = "", params object[] becauseArgs) + { + return BeEquivalentTo(subtree, true, options => options, because, becauseArgs); + } + + /// + /// Recursively asserts that the current contains at least the properties or elements of the specified . + /// + /// The subtree to search for + /// The options to consider while asserting values + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Use this method to match the current against an arbitrary subtree, + /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, + /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. + /// + /// This example asserts the values of multiple properties of a child object within a JSON document, using a specified double precision. + /// + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', value: 0.99 } }"); + /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', value: 1.0 } }"), options => options + /// .Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1)) + /// .WhenTypeIs<double>()); + /// + /// + /// + /// This example asserts that a within a has at least one element with at least the given properties, using a specified double precision. + /// + /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', value: 0.99 }, { id: 3, type: 'other-type', value: 3 } ] }"); + /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', value: 1 } ] }"), options => options + /// .Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1)) + /// .WhenTypeIs<double>()); + /// + /// + public AndConstraint ContainSubtree(JToken subtree, + Func, IJsonAssertionOptions> config, + string because = "", + params object[] becauseArgs) + { + return BeEquivalentTo(subtree, true, config, because, becauseArgs); + } + + private static JToken Parse(string json, string paramName) + { + try { - JToken subtreeToken; - try - { - subtreeToken = JToken.Parse(subtree); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse expected JSON string:{Environment.NewLine}" + - $"{subtree}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(subtree), ex); - } - - return ContainSubtree(subtreeToken, because, becauseArgs); + return JToken.Parse(json); } - - private bool JTokenContainsSubtree(JToken token, JToken subtree) + catch (Exception ex) { - switch (subtree.Type) - { - case JTokenType.Object: - { - var sub = (JObject)subtree; - var obj = token as JObject; - if (obj == null) - return false; - foreach (var subProp in sub.Properties()) - { - var prop = obj.Property(subProp.Name); - if (prop == null) - return false; - if (!JTokenContainsSubtree(prop.Value, subProp.Value)) - return false; - } - return true; - } - case JTokenType.Array: - { - var sub = (JArray)subtree; - var arr = token as JArray; - if (arr == null) - return false; - foreach (var subItem in sub) - { - if (!arr.Any(item => JTokenContainsSubtree(item, subItem))) - return false; - } - return true; - } - default: - return JToken.DeepEquals(token, subtree); - - } + throw new ArgumentException( + $"Unable to parse {paramName} JSON string:{Environment.NewLine}" + + $"{json}{Environment.NewLine}" + + "Check inner exception for more details.", + paramName, ex); } + } - public string Format(JToken value, bool useLineBreaks = false) +#pragma warning disable CA1822 // Making this method static is a breaking chan + public string Format(JToken value, bool useLineBreaks = false) + { + // SMELL: Why is this method necessary at all? + // SMELL: Why aren't we using the Formatter class directly? + var output = new FormattedObjectGraph(maxLines: 100); + + new JTokenFormatter().Format(value, output, new FormattingContext { - return new JTokenFormatter().Format(value, new FormattingContext - { - UseLineBreaks = useLineBreaks - }, null); - } + UseLineBreaks = useLineBreaks + }, null); + + return output.ToString(); } -} \ No newline at end of file +#pragma warning restore CA1822 // Making this method static is a breaking chan +} diff --git a/Src/FluentAssertions.Json/JTokenDifferentiator.cs b/Src/FluentAssertions.Json/JTokenDifferentiator.cs index 2dda4050..b3344b71 100644 --- a/Src/FluentAssertions.Json/JTokenDifferentiator.cs +++ b/Src/FluentAssertions.Json/JTokenDifferentiator.cs @@ -1,251 +1,268 @@ using System; using System.Collections.Generic; using System.Linq; +using FluentAssertions.Execution; +using FluentAssertions.Json.Common; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +internal sealed class JTokenDifferentiator { - internal static class JTokenDifferentiator + private readonly bool ignoreExtraProperties; + + private readonly Func, IJsonAssertionOptions> config; + private readonly JsonAssertionOptions options; + + public JTokenDifferentiator(bool ignoreExtraProperties, + Func, IJsonAssertionOptions> config) { - public static Difference FindFirstDifference(JToken actual, JToken expected) - { - var path = new JPath(); - - if (actual == expected) - { - return null; - } + this.ignoreExtraProperties = ignoreExtraProperties; + this.config = config; + this.options = (JsonAssertionOptions)config(new JsonAssertionOptions()); + } - if (actual == null) - { - return new Difference(DifferenceKind.ActualIsNull, path); - } + public Difference FindFirstDifference(JToken actual, JToken expected) + { + var path = new JPath(); - if (expected == null) - { - return new Difference(DifferenceKind.ExpectedIsNull, path); - } - - return FindFirstDifference(actual, expected, path); + if (actual == expected) + { + return null; } - private static Difference FindFirstDifference(JToken actual, JToken expected, JPath path) + if (actual == null) { - switch (actual) - { - case JArray actualArray: - return FindJArrayDifference(actualArray, expected, path); - case JObject actualObbject: - return FindJObjectDifference(actualObbject, expected, path); - case JProperty actualProperty: - return FindJPropertyDifference(actualProperty, expected, path); - case JValue actualValue: - return FindValueDifference(actualValue, expected, path); - default: - throw new NotSupportedException(); - } + return new Difference(DifferenceKind.ActualIsNull, path); } - private static Difference FindJArrayDifference(JArray actualArray, JToken expected, JPath path) + if (expected == null) { - if (!(expected is JArray expectedArray)) - { - return new Difference(DifferenceKind.OtherType, path); - } - - return CompareItems(actualArray, expectedArray, path); + return new Difference(DifferenceKind.ExpectedIsNull, path); } - private static Difference CompareItems(JArray actual, JArray expected, JPath path) + if (!options.IsStrictlyOrdered) { - JEnumerable actualChildren = actual.Children(); - JEnumerable expectedChildren = expected.Children(); - - if (actualChildren.Count() != expectedChildren.Count()) - { - return new Difference(DifferenceKind.DifferentLength, path); - } + actual = actual.Normalize(); + expected = expected.Normalize(); + } - for (int i = 0; i < actualChildren.Count(); i++) - { - Difference firstDifference = FindFirstDifference(actualChildren.ElementAt(i), expectedChildren.ElementAt(i), - path.AddIndex(i)); + return FindFirstDifference(actual, expected, path); + } - if (firstDifference != null) - { - return firstDifference; - } - } + private Difference FindFirstDifference(JToken actual, JToken expected, JPath path) + { + return actual switch + { + JArray actualArray => FindJArrayDifference(actualArray, expected, path), + JObject actualObject => FindJObjectDifference(actualObject, expected, path), + JProperty actualProperty => FindJPropertyDifference(actualProperty, expected, path), + JValue actualValue => FindValueDifference(actualValue, expected, path), + _ => throw new NotSupportedException(), + }; + } - return null; + private Difference FindJArrayDifference(JArray actualArray, JToken expected, JPath path) + { + if (expected is not JArray expectedArray) + { + return new Difference(DifferenceKind.OtherType, path, Describe(actualArray.Type), Describe(expected.Type)); } - private static Difference FindJObjectDifference(JObject actual, JToken expected, JPath path) + if (ignoreExtraProperties) { - if (!(expected is JObject expectedObject)) - { - return new Difference(DifferenceKind.OtherType, path); - } - - return CompareProperties(actual?.Properties(), expectedObject.Properties(), path); + return CompareExpectedItems(actualArray, expectedArray, path); } - - private static Difference CompareProperties(IEnumerable actual, IEnumerable expected, JPath path) + else { - var actualDictionary = actual?.ToDictionary(p => p.Name, p => p.Value) ?? new Dictionary(); - var expectedDictionary = expected?.ToDictionary(p => p.Name, p => p.Value) ?? new Dictionary(); + return CompareItems(actualArray, expectedArray, path); + } + } - foreach (KeyValuePair expectedPair in expectedDictionary) + private Difference CompareExpectedItems(JArray actual, JArray expected, JPath path) + { + JToken[] actualChildren = actual.Children().ToArray(); + JToken[] expectedChildren = expected.Children().ToArray(); + + int matchingIndex = 0; + for (int expectedIndex = 0; expectedIndex < expectedChildren.Length; expectedIndex++) + { + var expectedChild = expectedChildren[expectedIndex]; + bool match = false; + for (int actualIndex = matchingIndex; actualIndex < actualChildren.Length; actualIndex++) { - if (!actualDictionary.ContainsKey(expectedPair.Key)) + var difference = FindFirstDifference(actualChildren[actualIndex], expectedChild); + + if (difference == null) { - return new Difference(DifferenceKind.ActualMissesProperty, path.AddProperty(expectedPair.Key)); + match = true; + matchingIndex = actualIndex + 1; + break; } } - foreach (KeyValuePair actualPair in actualDictionary) + if (!match) { - if (!expectedDictionary.ContainsKey(actualPair.Key)) + if (matchingIndex >= actualChildren.Length) { - return new Difference(DifferenceKind.ExpectedMissesProperty, path.AddProperty(actualPair.Key)); + if (actualChildren.Any(actualChild => FindFirstDifference(actualChild, expectedChild) == null)) + { + return new Difference(DifferenceKind.WrongOrder, path.AddIndex(expectedIndex)); + } + + return new Difference(DifferenceKind.ActualMissesElement, path.AddIndex(expectedIndex)); } + + return FindFirstDifference(actualChildren[matchingIndex], expectedChild, + path.AddIndex(expectedIndex)); } + } - foreach (KeyValuePair expectedPair in expectedDictionary) - { - JToken actualValue = actualDictionary[expectedPair.Key]; + return null; + } - Difference firstDifference = FindFirstDifference(actualValue, expectedPair.Value, - path.AddProperty(expectedPair.Key)); - - if (firstDifference != null) - { - return firstDifference; - } - } + private Difference CompareItems(JArray actual, JArray expected, JPath path) + { + JToken[] actualChildren = actual.Children().ToArray(); + JToken[] expectedChildren = expected.Children().ToArray(); - return null; + if (actualChildren.Length != expectedChildren.Length) + { + return new Difference(DifferenceKind.DifferentLength, path, actualChildren.Length, expectedChildren.Length); } - private static Difference FindJPropertyDifference(JProperty actualProperty, JToken expected, JPath path) + for (int i = 0; i < actualChildren.Length; i++) { - if (!(expected is JProperty expectedProperty)) - { - return new Difference(DifferenceKind.OtherType, path); - } + Difference firstDifference = FindFirstDifference(actualChildren[i], expectedChildren[i], path.AddIndex(i)); - if (actualProperty.Name != expectedProperty.Name) + if (firstDifference != null) { - return new Difference(DifferenceKind.OtherName, path); + return firstDifference; } - - return FindFirstDifference(actualProperty.Value, expectedProperty.Value, path); } - private static Difference FindValueDifference(JValue actualValue, JToken expected, JPath path) + return null; + } + + private Difference FindJObjectDifference(JObject actual, JToken expected, JPath path) + { + if (expected is not JObject expectedObject) { - if (!(expected is JValue expectedValue)) + return new Difference(DifferenceKind.OtherType, path, Describe(actual.Type), Describe(expected.Type)); + } + + return CompareProperties(actual?.Properties(), expectedObject.Properties(), path); + } + + private Difference CompareProperties(IEnumerable actual, IEnumerable expected, JPath path) + { + var actualDictionary = actual?.ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); + var expectedDictionary = expected?.ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); + + foreach (KeyValuePair expectedPair in expectedDictionary) + { + if (!actualDictionary.ContainsKey(expectedPair.Key)) { - return new Difference(DifferenceKind.OtherType, path); + return new Difference(DifferenceKind.ActualMissesProperty, path.AddProperty(expectedPair.Key)); } - - return CompareValues(actualValue, expectedValue, path); } - private static Difference CompareValues(JValue actual, JValue expected, JPath path) + foreach (KeyValuePair actualPair in actualDictionary) { - if (actual.Type != expected.Type) + if (!ignoreExtraProperties && !expectedDictionary.ContainsKey(actualPair.Key)) { - return new Difference(DifferenceKind.OtherType, path); + return new Difference(DifferenceKind.ExpectedMissesProperty, path.AddProperty(actualPair.Key)); } - - if (!actual.Equals(expected)) + } + + foreach (KeyValuePair expectedPair in expectedDictionary) + { + JToken actualValue = actualDictionary[expectedPair.Key]; + + Difference firstDifference = FindFirstDifference(actualValue, expectedPair.Value, + path.AddProperty(expectedPair.Key)); + + if (firstDifference != null) { - return new Difference(DifferenceKind.OtherValue, path); + return firstDifference; } - - return null; } + + return null; } - internal class Difference + private Difference FindJPropertyDifference(JProperty actualProperty, JToken expected, JPath path) { - public Difference(DifferenceKind kind, JPath path) + if (expected is not JProperty expectedProperty) { - Kind = kind; - Path = path; + return new Difference(DifferenceKind.OtherType, path, Describe(actualProperty.Type), Describe(expected.Type)); } - private DifferenceKind Kind { get; } - - private JPath Path { get; } - - public override string ToString() + if (actualProperty.Name != expectedProperty.Name) { - switch (Kind) - { - case DifferenceKind.ActualIsNull: - return "is null"; - case DifferenceKind.ExpectedIsNull: - return "is not null"; - case DifferenceKind.OtherType: - return $"has a different type at {Path}"; - case DifferenceKind.OtherName: - return $"has a different name at {Path}"; - case DifferenceKind.OtherValue: - return $"has a different value at {Path}"; - case DifferenceKind.DifferentLength: - return $"has a different length at {Path}"; - case DifferenceKind.ActualMissesProperty: - return $"misses property {Path}"; - case DifferenceKind.ExpectedMissesProperty: - return $"has extra property {Path}"; - default: - throw new ArgumentOutOfRangeException(); - } + return new Difference(DifferenceKind.OtherName, path); } + + return FindFirstDifference(actualProperty.Value, expectedProperty.Value, path); } - internal class JPath + private Difference FindValueDifference(JValue actualValue, JToken expected, JPath path) { - private readonly List nodes = new List(); - - public JPath() + if (expected is not JValue expectedValue) { - nodes.Add("$"); + return new Difference(DifferenceKind.OtherType, path, Describe(actualValue.Type), Describe(expected.Type)); } - private JPath(JPath existingPath, string extraNode) - { - nodes.AddRange(existingPath.nodes); - nodes.Add(extraNode); - } + return CompareValues(actualValue, expectedValue, path); + } - public JPath AddProperty(string name) + private Difference CompareValues(JValue actual, JValue expected, JPath path) + { + if (actual.Type != expected.Type) { - return new JPath(this, $".{name}"); + return new Difference(DifferenceKind.OtherType, path, Describe(actual.Type), Describe(expected.Type)); } - public JPath AddIndex(int index) + bool hasMismatches; + using (var scope = new AssertionScope()) { - return new JPath(this, $"[{index}]"); + actual.Value.Should().BeEquivalentTo(expected.Value, options => + (JsonAssertionOptions)config.Invoke(new JsonAssertionOptions(options))); + + hasMismatches = scope.Discard().Length > 0; } - public override string ToString() + if (hasMismatches) { - return string.Join("", nodes); + return new Difference(DifferenceKind.OtherValue, path); } + + return null; } - internal enum DifferenceKind + private static string Describe(JTokenType jTokenType) { - ActualIsNull, - ExpectedIsNull, - OtherType, - OtherName, - OtherValue, - DifferentLength, - ActualMissesProperty, - ExpectedMissesProperty + return jTokenType switch + { + JTokenType.None => "type none", + JTokenType.Object => "an object", + JTokenType.Array => "an array", + JTokenType.Constructor => "a constructor", + JTokenType.Property => "a property", + JTokenType.Comment => "a comment", + JTokenType.Integer => "an integer", + JTokenType.Float => "a float", + JTokenType.String => "a string", + JTokenType.Boolean => "a boolean", + JTokenType.Null => "type null", + JTokenType.Undefined => "type undefined", + JTokenType.Date => "a date", + JTokenType.Raw => "type raw", + JTokenType.Bytes => "type bytes", + JTokenType.Guid => "a GUID", + JTokenType.Uri => "a URI", + JTokenType.TimeSpan => "a timespan", + _ => throw new ArgumentOutOfRangeException(nameof(jTokenType), jTokenType, null), + }; } -} \ No newline at end of file +} diff --git a/Src/FluentAssertions.Json/JTokenFormatter.cs b/Src/FluentAssertions.Json/JTokenFormatter.cs index dcb86fdb..10309245 100644 --- a/Src/FluentAssertions.Json/JTokenFormatter.cs +++ b/Src/FluentAssertions.Json/JTokenFormatter.cs @@ -1,47 +1,45 @@ -using System.Collections.Generic; using FluentAssertions.Formatting; using FluentAssertions.Json.Common; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// A for . +/// +public class JTokenFormatter : IValueFormatter { /// - /// A for . + /// Indicates whether the current can handle the specified . /// - public class JTokenFormatter : IValueFormatter + /// The value for which to create a . + /// + /// true if the current can handle the specified value; otherwise, false. + /// + public bool CanHandle(object value) + { + return value is JToken; + } + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) { - /// - /// Indicates whether the current can handle the specified . - /// - /// The value for which to create a . - /// - /// true if the current can handle the specified value; otherwise, false. - /// - public bool CanHandle(object value) + var jToken = value as JToken; + + if (context.UseLineBreaks) { - return value is JToken; + var result = jToken?.ToString(Newtonsoft.Json.Formatting.Indented); + if (result is not null) + { + formattedGraph.AddFragmentOnNewLine(result); + } + else + { + formattedGraph.AddFragment(""); + } } - - /// - /// Returns a that represents this instance. - /// - /// The value for which to create a . - /// - /// - /// A collection of objects that - /// - /// - /// The level of nesting for the supplied value. This is used for indenting the format string for objects that have - /// no override. - /// - /// - /// A that represents this instance. - /// - public string Format(object value, FormattingContext context, FormatChild formatChild) + else { - var jToken = (JToken)value; - string result = context.UseLineBreaks ? jToken?.ToString(Newtonsoft.Json.Formatting.Indented) : jToken?.ToString().RemoveNewLines(); - return result ?? ""; + formattedGraph.AddFragment(jToken?.ToString().RemoveNewLines() ?? ""); } } -} \ No newline at end of file +} diff --git a/Src/FluentAssertions.Json/JetBrainsAnnotations.cs b/Src/FluentAssertions.Json/JetBrainsAnnotations.cs deleted file mode 100644 index da7f68fa..00000000 --- a/Src/FluentAssertions.Json/JetBrainsAnnotations.cs +++ /dev/null @@ -1,1066 +0,0 @@ -/* MIT License - -Copyright (c) 2016 JetBrains http://www.jetbrains.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -using System; - -#pragma warning disable 1591 -// ReSharper disable UnusedMember.Global -// ReSharper disable MemberCanBePrivate.Global -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable IntroduceOptionalParameters.Global -// ReSharper disable MemberCanBeProtected.Global -// ReSharper disable InconsistentNaming - -namespace JetBrains.Annotations -{ - /// - /// Indicates that the value of the marked element could be null sometimes, - /// so the check for null is necessary before its usage. - /// - /// - /// [CanBeNull] object Test() => null; - /// - /// void UseTest() { - /// var p = Test(); - /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' - /// } - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | - AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] - internal sealed class CanBeNullAttribute : Attribute { } - - /// - /// Indicates that the value of the marked element could never be null. - /// - /// - /// [NotNull] object Foo() { - /// return null; // Warning: Possible 'null' assignment - /// } - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | - AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] - internal sealed class NotNullAttribute : Attribute { } - - /// - /// Can be appplied to symbols of types derived from IEnumerable as well as to symbols of Task - /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property - /// or of the Lazy.Value property can never be null. - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field)] - internal sealed class ItemNotNullAttribute : Attribute { } - - /// - /// Can be appplied to symbols of types derived from IEnumerable as well as to symbols of Task - /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property - /// or of the Lazy.Value property can be null. - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field)] - internal sealed class ItemCanBeNullAttribute : Attribute { } - - /// - /// Implicitly apply [NotNull]/[ItemNotNull] annotation to all the of type members and parameters - /// in particular scope where this annotation is used (type declaration or whole assembly). - /// - [AttributeUsage( - AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Assembly)] - internal sealed class ImplicitNotNullAttribute : Attribute { } - - /// - /// Indicates that the marked method builds string by format pattern and (optional) arguments. - /// Parameter, which contains format string, should be given in constructor. The format string - /// should be in -like form. - /// - /// - /// [StringFormatMethod("message")] - /// void ShowError(string message, params object[] args) { /* do something */ } - /// - /// void Foo() { - /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string - /// } - /// - [AttributeUsage( - AttributeTargets.Constructor | AttributeTargets.Method | - AttributeTargets.Property | AttributeTargets.Delegate)] - internal sealed class StringFormatMethodAttribute : Attribute - { - /// - /// Specifies which parameter of an annotated method should be treated as format-string - /// - public StringFormatMethodAttribute([NotNull] string formatParameterName) - { - FormatParameterName = formatParameterName; - } - - [NotNull] - public string FormatParameterName { get; private set; } - } - - /// - /// For a parameter that is expected to be one of the limited set of values. - /// Specify fields of which type should be used as values for this parameter. - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] - internal sealed class ValueProviderAttribute : Attribute - { - public ValueProviderAttribute([NotNull] string name) - { - Name = name; - } - - [NotNull] - public string Name { get; private set; } - } - - /// - /// Indicates that the function argument should be string literal and match one - /// of the parameters of the caller function. For example, ReSharper annotates - /// the parameter of . - /// - /// - /// void Foo(string param) { - /// if (param == null) - /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol - /// } - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class InvokerParameterNameAttribute : Attribute { } - - /// - /// Indicates that the method is contained in a type that implements - /// System.ComponentModel.INotifyPropertyChanged interface and this method - /// is used to notify that some property value changed. - /// - /// - /// The method should be non-static and conform to one of the supported signatures: - /// - /// NotifyChanged(string) - /// NotifyChanged(params string[]) - /// NotifyChanged{T}(Expression{Func{T}}) - /// NotifyChanged{T,U}(Expression{Func{T,U}}) - /// SetProperty{T}(ref T, T, string) - /// - /// - /// - /// public class Foo : INotifyPropertyChanged { - /// public event PropertyChangedEventHandler PropertyChanged; - /// - /// [NotifyPropertyChangedInvocator] - /// protected virtual void NotifyChanged(string propertyName) { ... } - /// - /// string _name; - /// - /// public string Name { - /// get { return _name; } - /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } - /// } - /// } - /// - /// Examples of generated notifications: - /// - /// NotifyChanged("Property") - /// NotifyChanged(() => Property) - /// NotifyChanged((VM x) => x.Property) - /// SetProperty(ref myField, value, "Property") - /// - /// - [AttributeUsage(AttributeTargets.Method)] - internal sealed class NotifyPropertyChangedInvocatorAttribute : Attribute - { - public NotifyPropertyChangedInvocatorAttribute() { } - public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) - { - ParameterName = parameterName; - } - - [CanBeNull] - public string ParameterName { get; private set; } - } - - /// - /// Describes dependency between method input and output. - /// - /// - ///

Function Definition Table syntax:

- /// - /// FDT ::= FDTRow [;FDTRow]* - /// FDTRow ::= Input => Output | Output <= Input - /// Input ::= ParameterName: Value [, Input]* - /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} - /// Value ::= true | false | null | notnull | canbenull - /// - /// If method has single input parameter, it's name could be omitted.
- /// Using halt (or void/nothing, which is the same) - /// for method output means that the methos doesn't return normally.
- /// canbenull annotation is only applicable for output parameters.
- /// You can use multiple [ContractAnnotation] for each FDT row, - /// or use single attribute with rows separated by semicolon.
- ///
- /// - /// - /// [ContractAnnotation("=> halt")] - /// public void TerminationMethod() - /// - /// - /// [ContractAnnotation("halt <= condition: false")] - /// public void Assert(bool condition, string text) // regular assertion method - /// - /// - /// [ContractAnnotation("s:null => true")] - /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() - /// - /// - /// // A method that returns null if the parameter is null, - /// // and not null if the parameter is not null - /// [ContractAnnotation("null => null; notnull => notnull")] - /// public object Transform(object data) - /// - /// - /// [ContractAnnotation("s:null=>false; =>true,result:notnull; =>false, result:null")] - /// public bool TryParse(string s, out Person result) - /// - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - internal sealed class ContractAnnotationAttribute : Attribute - { - public ContractAnnotationAttribute([NotNull] string contract) - : this(contract, false) { } - - public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) - { - Contract = contract; - ForceFullStates = forceFullStates; - } - - [NotNull] - public string Contract { get; private set; } - public bool ForceFullStates { get; private set; } - } - - /// - /// Indicates that marked element should be localized or not. - /// - /// - /// [LocalizationRequiredAttribute(true)] - /// class Foo { - /// string str = "my string"; // Warning: Localizable string - /// } - /// - [AttributeUsage(AttributeTargets.All)] - internal sealed class LocalizationRequiredAttribute : Attribute - { - public LocalizationRequiredAttribute() : this(true) { } - public LocalizationRequiredAttribute(bool required) - { - Required = required; - } - - public bool Required { get; private set; } - } - - /// - /// Indicates that the value of the marked type (or its derivatives) - /// cannot be compared using '==' or '!=' operators and Equals() - /// should be used instead. However, using '==' or '!=' for comparison - /// with null is always permitted. - /// - /// - /// [CannotApplyEqualityOperator] - /// class NoEquality { } - /// - /// class UsesNoEquality { - /// void Test() { - /// var ca1 = new NoEquality(); - /// var ca2 = new NoEquality(); - /// if (ca1 != null) { // OK - /// bool condition = ca1 == ca2; // Warning - /// } - /// } - /// } - /// - [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] - internal sealed class CannotApplyEqualityOperatorAttribute : Attribute { } - - /// - /// When applied to a target attribute, specifies a requirement for any type marked - /// with the target attribute to implement or inherit specific type or types. - /// - /// - /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement - /// class ComponentAttribute : Attribute { } - /// - /// [Component] // ComponentAttribute requires implementing IComponent interface - /// class MyComponent : IComponent { } - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - [BaseTypeRequired(typeof(Attribute))] - internal sealed class BaseTypeRequiredAttribute : Attribute - { - public BaseTypeRequiredAttribute([NotNull] Type baseType) - { - BaseType = baseType; - } - - [NotNull] - public Type BaseType { get; private set; } - } - - /// - /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), - /// so this symbol will not be marked as unused (as well as by other usage inspections). - /// - [AttributeUsage(AttributeTargets.All)] - internal sealed class UsedImplicitlyAttribute : Attribute - { - public UsedImplicitlyAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } - - public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) { } - - public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) { } - - public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) - { - UseKindFlags = useKindFlags; - TargetFlags = targetFlags; - } - - public ImplicitUseKindFlags UseKindFlags { get; private set; } - public ImplicitUseTargetFlags TargetFlags { get; private set; } - } - - /// - /// Should be used on attributes and causes ReSharper to not mark symbols marked with such attributes - /// as unused (as well as by other usage inspections) - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter)] - internal sealed class MeansImplicitUseAttribute : Attribute - { - public MeansImplicitUseAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } - - public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) { } - - public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) { } - - public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) - { - UseKindFlags = useKindFlags; - TargetFlags = targetFlags; - } - - [UsedImplicitly] - public ImplicitUseKindFlags UseKindFlags { get; private set; } - [UsedImplicitly] - public ImplicitUseTargetFlags TargetFlags { get; private set; } - } - - [Flags] - internal enum ImplicitUseKindFlags - { - Default = Access | Assign | InstantiatedWithFixedConstructorSignature, - /// Only entity marked with attribute considered used. - Access = 1, - /// Indicates implicit assignment to a member. - Assign = 2, - /// - /// Indicates implicit instantiation of a type with fixed constructor signature. - /// That means any unused constructor parameters won't be reported as such. - /// - InstantiatedWithFixedConstructorSignature = 4, - /// Indicates implicit instantiation of a type. - InstantiatedNoFixedConstructorSignature = 8, - } - - /// - /// Specify what is considered used implicitly when marked - /// with or . - /// - [Flags] - internal enum ImplicitUseTargetFlags - { - Default = Itself, - Itself = 1, - /// Members of entity marked with attribute are considered used. - Members = 2, - /// Entity marked with attribute and all its members considered used. - WithMembers = Itself | Members - } - - /// - /// This attribute is intended to mark publicly available API - /// which should not be removed and so is treated as used. - /// - [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] - internal sealed class PublicAPIAttribute : Attribute - { - public PublicAPIAttribute() { } - public PublicAPIAttribute([NotNull] string comment) - { - Comment = comment; - } - - [CanBeNull] - public string Comment { get; private set; } - } - - /// - /// Tells code analysis engine if the parameter is completely handled when the invoked method is on stack. - /// If the parameter is a delegate, indicates that delegate is executed while the method is executed. - /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class InstantHandleAttribute : Attribute { } - - /// - /// Indicates that a method does not make any observable state changes. - /// The same as System.Diagnostics.Contracts.PureAttribute. - /// - /// - /// [Pure] int Multiply(int x, int y) => x * y; - /// - /// void M() { - /// Multiply(123, 42); // Waring: Return value of pure method is not used - /// } - /// - [AttributeUsage(AttributeTargets.Method)] - internal sealed class PureAttribute : Attribute { } - - /// - /// Indicates that the return value of method invocation must be used. - /// - [AttributeUsage(AttributeTargets.Method)] - internal sealed class MustUseReturnValueAttribute : Attribute - { - public MustUseReturnValueAttribute() { } - public MustUseReturnValueAttribute([NotNull] string justification) - { - Justification = justification; - } - - [CanBeNull] - public string Justification { get; private set; } - } - - /// - /// Indicates the type member or parameter of some type, that should be used instead of all other ways - /// to get the value that type. This annotation is useful when you have some "context" value evaluated - /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. - /// - /// - /// class Foo { - /// [ProvidesContext] IBarService _barService = ...; - /// - /// void ProcessNode(INode node) { - /// DoSomething(node, node.GetGlobalServices().Bar); - /// // ^ Warning: use value of '_barService' field - /// } - /// } - /// - [AttributeUsage( - AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | - AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] - internal sealed class ProvidesContextAttribute : Attribute { } - - /// - /// Indicates that a parameter is a path to a file or a folder within a web project. - /// Path can be relative or absolute, starting from web root (~). - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class PathReferenceAttribute : Attribute - { - public PathReferenceAttribute() { } - public PathReferenceAttribute([NotNull, PathReference] string basePath) - { - BasePath = basePath; - } - - [CanBeNull] - public string BasePath { get; private set; } - } - - /// - /// An extension method marked with this attribute is processed by ReSharper code completion - /// as a 'Source Template'. When extension method is completed over some expression, it's source code - /// is automatically expanded like a template at call site. - /// - /// - /// Template method body can contain valid source code and/or special comments starting with '$'. - /// Text inside these comments is added as source code when the template is applied. Template parameters - /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. - /// Use the attribute to specify macros for parameters. - /// - /// - /// In this example, the 'forEach' method is a source template available over all values - /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: - /// - /// [SourceTemplate] - /// public static void forEach<T>(this IEnumerable<T> xs) { - /// foreach (var x in xs) { - /// //$ $END$ - /// } - /// } - /// - /// - [AttributeUsage(AttributeTargets.Method)] - internal sealed class SourceTemplateAttribute : Attribute { } - - /// - /// Allows specifying a macro for a parameter of a source template. - /// - /// - /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression - /// is defined in the property. When applied on a method, the target - /// template parameter is defined in the property. To apply the macro silently - /// for the parameter, set the property value = -1. - /// - /// - /// Applying the attribute on a source template method: - /// - /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] - /// public static void forEach<T>(this IEnumerable<T> collection) { - /// foreach (var item in collection) { - /// //$ $END$ - /// } - /// } - /// - /// Applying the attribute on a template method parameter: - /// - /// [SourceTemplate] - /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { - /// /*$ var $x$Id = "$newguid$" + x.ToString(); - /// x.DoSomething($x$Id); */ - /// } - /// - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] - internal sealed class MacroAttribute : Attribute - { - /// - /// Allows specifying a macro that will be executed for a source template - /// parameter when the template is expanded. - /// - public string Expression { get; set; } - - /// - /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. - /// - /// - /// If the target parameter is used several times in the template, only one occurrence becomes editable; - /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, - /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. - /// > - public int Editable { get; set; } - - /// - /// Identifies the target parameter of a source template if the - /// is applied on a template method. - /// - public string Target { get; set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute - { - public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute - { - public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class AspMvcAreaViewLocationFormatAttribute : Attribute - { - public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class AspMvcMasterLocationFormatAttribute : Attribute - { - public AspMvcMasterLocationFormatAttribute(string format) - { - Format = format; - } - - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class AspMvcPartialViewLocationFormatAttribute : Attribute - { - public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class AspMvcViewLocationFormatAttribute : Attribute - { - public AspMvcViewLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; private set; } - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC action. If applied to a method, the MVC action name is calculated - /// implicitly from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - internal sealed class AspMvcActionAttribute : Attribute - { - public AspMvcActionAttribute() { } - public AspMvcActionAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [CanBeNull] - public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC area. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class AspMvcAreaAttribute : Attribute - { - public AspMvcAreaAttribute() { } - public AspMvcAreaAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [CanBeNull] - public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is - /// an MVC controller. If applied to a method, the MVC controller name is calculated - /// implicitly from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - internal sealed class AspMvcControllerAttribute : Attribute - { - public AspMvcControllerAttribute() { } - public AspMvcControllerAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [CanBeNull] - public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC Master. Use this attribute - /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class AspMvcMasterAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC model type. Use this attribute - /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class AspMvcModelTypeAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC - /// partial view. If applied to a method, the MVC partial view name is calculated implicitly - /// from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - internal sealed class AspMvcPartialViewAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - internal sealed class AspMvcSuppressViewErrorAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class AspMvcDisplayTemplateAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC editor template. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class AspMvcEditorTemplateAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC template. - /// Use this attribute for custom wrappers similar to - /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class AspMvcTemplateAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly - /// from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Controller.View(Object). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - internal sealed class AspMvcViewAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC view component name. - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class AspMvcViewComponentAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC view component view. If applied to a method, the MVC view component view name is default. - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - internal sealed class AspMvcViewComponentViewAttribute : Attribute { } - - /// - /// ASP.NET MVC attribute. When applied to a parameter of an attribute, - /// indicates that this parameter is an MVC action name. - /// - /// - /// [ActionName("Foo")] - /// public ActionResult Login(string returnUrl) { - /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK - /// return RedirectToAction("Bar"); // Error: Cannot resolve action - /// } - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] - internal sealed class AspMvcActionSelectorAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] - internal sealed class HtmlElementAttributesAttribute : Attribute - { - public HtmlElementAttributesAttribute() { } - public HtmlElementAttributesAttribute([NotNull] string name) - { - Name = name; - } - - [CanBeNull] - public string Name { get; private set; } - } - - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - internal sealed class HtmlAttributeValueAttribute : Attribute - { - public HtmlAttributeValueAttribute([NotNull] string name) - { - Name = name; - } - - [NotNull] - public string Name { get; private set; } - } - - /// - /// Razor attribute. Indicates that a parameter or a method is a Razor section. - /// Use this attribute for custom wrappers similar to - /// System.Web.WebPages.WebPageBase.RenderSection(String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - internal sealed class RazorSectionAttribute : Attribute { } - - /// - /// Indicates how method, constructor invocation or property access - /// over collection type affects content of the collection. - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] - internal sealed class CollectionAccessAttribute : Attribute - { - public CollectionAccessAttribute(CollectionAccessType collectionAccessType) - { - CollectionAccessType = collectionAccessType; - } - - public CollectionAccessType CollectionAccessType { get; private set; } - } - - [Flags] - internal enum CollectionAccessType - { - /// Method does not use or modify content of the collection. - None = 0, - /// Method only reads content of the collection but does not modify it. - Read = 1, - /// Method can change content of the collection but does not add new elements. - ModifyExistingContent = 2, - /// Method can add new elements to the collection. - UpdatedContent = ModifyExistingContent | 4 - } - - /// - /// Indicates that the marked method is assertion method, i.e. it halts control flow if - /// one of the conditions is satisfied. To set the condition, mark one of the parameters with - /// attribute. - /// - [AttributeUsage(AttributeTargets.Method)] - internal sealed class AssertionMethodAttribute : Attribute { } - - /// - /// Indicates the condition parameter of the assertion method. The method itself should be - /// marked by attribute. The mandatory argument of - /// the attribute is the assertion type. - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class AssertionConditionAttribute : Attribute - { - public AssertionConditionAttribute(AssertionConditionType conditionType) - { - ConditionType = conditionType; - } - - public AssertionConditionType ConditionType { get; private set; } - } - - /// - /// Specifies assertion type. If the assertion method argument satisfies the condition, - /// then the execution continues. Otherwise, execution is assumed to be halted. - /// - internal enum AssertionConditionType - { - /// Marked parameter should be evaluated to true. - IS_TRUE = 0, - /// Marked parameter should be evaluated to false. - IS_FALSE = 1, - /// Marked parameter should be evaluated to null value. - IS_NULL = 2, - /// Marked parameter should be evaluated to not null value. - IS_NOT_NULL = 3, - } - - /// - /// Indicates that the marked method unconditionally terminates control flow execution. - /// For example, it could unconditionally throw exception. - /// - [Obsolete("Use [ContractAnnotation('=> halt')] instead")] - [AttributeUsage(AttributeTargets.Method)] - internal sealed class TerminatesProgramAttribute : Attribute { } - - /// - /// Indicates that method is pure LINQ method, with postponed enumeration (like Enumerable.Select, - /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters - /// of delegate type by analyzing LINQ method chains. - /// - [AttributeUsage(AttributeTargets.Method)] - internal sealed class LinqTunnelAttribute : Attribute { } - - /// - /// Indicates that IEnumerable, passed as parameter, is not enumerated. - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class NoEnumerationAttribute : Attribute { } - - /// - /// Indicates that parameter is regular expression pattern. - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class RegexPatternAttribute : Attribute { } - - /// - /// XAML attribute. Indicates the type that has ItemsSource property and should be treated - /// as ItemsControl-derived type, to enable inner items DataContext type resolve. - /// - [AttributeUsage(AttributeTargets.Class)] - internal sealed class XamlItemsControlAttribute : Attribute { } - - /// - /// XAML attribute. Indicates the property of some BindingBase-derived type, that - /// is used to bind some item of ItemsControl-derived type. This annotation will - /// enable the DataContext type resolve for XAML bindings for such properties. - /// - /// - /// Property should have the tree ancestor of the ItemsControl type or - /// marked with the attribute. - /// - [AttributeUsage(AttributeTargets.Property)] - internal sealed class XamlItemBindingOfItemsControlAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - internal sealed class AspChildControlTypeAttribute : Attribute - { - public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) - { - TagName = tagName; - ControlType = controlType; - } - - [NotNull] - public string TagName { get; private set; } - [NotNull] - public Type ControlType { get; private set; } - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - internal sealed class AspDataFieldAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - internal sealed class AspDataFieldsAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Property)] - internal sealed class AspMethodPropertyAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - internal sealed class AspRequiredAttributeAttribute : Attribute - { - public AspRequiredAttributeAttribute([NotNull] string attribute) - { - Attribute = attribute; - } - - [NotNull] - public string Attribute { get; private set; } - } - - [AttributeUsage(AttributeTargets.Property)] - internal sealed class AspTypePropertyAttribute : Attribute - { - public bool CreateConstructorReferences { get; private set; } - - public AspTypePropertyAttribute(bool createConstructorReferences) - { - CreateConstructorReferences = createConstructorReferences; - } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class RazorImportNamespaceAttribute : Attribute - { - public RazorImportNamespaceAttribute([NotNull] string name) - { - Name = name; - } - - [NotNull] - public string Name { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class RazorInjectionAttribute : Attribute - { - public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) - { - Type = type; - FieldName = fieldName; - } - - [NotNull] - public string Type { get; private set; } - [NotNull] - public string FieldName { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - internal sealed class RazorDirectiveAttribute : Attribute - { - public RazorDirectiveAttribute([NotNull] string directive) - { - Directive = directive; - } - - [NotNull] - public string Directive { get; private set; } - } - - [AttributeUsage(AttributeTargets.Method)] - internal sealed class RazorHelperCommonAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Property)] - internal sealed class RazorLayoutAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Method)] - internal sealed class RazorWriteLiteralMethodAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Method)] - internal sealed class RazorWriteMethodAttribute : Attribute { } - - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class RazorWriteMethodParameterAttribute : Attribute { } - - /// - /// Prevents the Member Reordering feature from tossing members of the marked class. - /// - /// - /// The attribute must be mentioned in your member reordering patterns - /// - [AttributeUsage(AttributeTargets.All)] - internal sealed class NoReorder : Attribute { } -} \ No newline at end of file diff --git a/Src/FluentAssertions.Json/JsonAssertionExtensions.cs b/Src/FluentAssertions.Json/JsonAssertionExtensions.cs index b19fcd1c..b002c728 100644 --- a/Src/FluentAssertions.Json/JsonAssertionExtensions.cs +++ b/Src/FluentAssertions.Json/JsonAssertionExtensions.cs @@ -1,41 +1,40 @@ using System.Diagnostics; +using FluentAssertions.Execution; using JetBrains.Annotations; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// Contains extension methods for JToken assertions. +/// +[DebuggerNonUserCode] +public static class JsonAssertionExtensions { /// - /// Contains extension methods for JToken assertions. + /// Returns an object that can be used to assert the current . /// - [DebuggerNonUserCode] - public static class JsonAssertionExtensions + [Pure] + public static JTokenAssertions Should(this JToken jToken) { - /// - /// Returns an object that can be used to assert the current . - /// - [Pure] - public static JTokenAssertions Should(this JToken jToken) - { - return new JTokenAssertions(jToken); - } - - /// - /// Returns an object that can be used to assert the current . - /// - [Pure] - public static JTokenAssertions Should(this JObject jObject) - { - return new JTokenAssertions(jObject); - } + return new JTokenAssertions(jToken, AssertionChain.GetOrCreate()); + } - /// - /// Returns an object that can be used to assert the current . - /// - [Pure] - public static JTokenAssertions Should(this JValue jValue) - { - return new JTokenAssertions(jValue); - } + /// + /// Returns an object that can be used to assert the current . + /// + [Pure] + public static JTokenAssertions Should(this JObject jObject) + { + return new JTokenAssertions(jObject, AssertionChain.GetOrCreate()); + } + /// + /// Returns an object that can be used to assert the current . + /// + [Pure] + public static JTokenAssertions Should(this JValue jValue) + { + return new JTokenAssertions(jValue, AssertionChain.GetOrCreate()); } -} \ No newline at end of file +} diff --git a/Src/FluentAssertions.Json/JsonAssertionOptions.cs b/Src/FluentAssertions.Json/JsonAssertionOptions.cs new file mode 100644 index 00000000..87191674 --- /dev/null +++ b/Src/FluentAssertions.Json/JsonAssertionOptions.cs @@ -0,0 +1,32 @@ +using System; +using FluentAssertions.Equivalency; + +namespace FluentAssertions.Json; + +/// +/// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of +/// +public sealed class JsonAssertionOptions : EquivalencyOptions, IJsonAssertionOptions +{ + internal JsonAssertionOptions() + { + } + + public JsonAssertionOptions(EquivalencyOptions equivalencyAssertionOptions) + : base(equivalencyAssertionOptions) + { + } + + internal bool IsStrictlyOrdered { get; private set; } = true; + + public new IJsonAssertionRestriction Using(Action> action) + { + return new JsonAssertionRestriction(base.Using(action)); + } + + public new IJsonAssertionOptions WithoutStrictOrdering() + { + IsStrictlyOrdered = false; + return this; + } +} diff --git a/Src/FluentAssertions.Json/JsonAssertionRestriction.cs b/Src/FluentAssertions.Json/JsonAssertionRestriction.cs new file mode 100644 index 00000000..bdac52e7 --- /dev/null +++ b/Src/FluentAssertions.Json/JsonAssertionRestriction.cs @@ -0,0 +1,17 @@ +namespace FluentAssertions.Json; + +public sealed class JsonAssertionRestriction : IJsonAssertionRestriction +{ + private readonly JsonAssertionOptions.Restriction restriction; + + internal JsonAssertionRestriction(JsonAssertionOptions.Restriction restriction) + { + this.restriction = restriction; + } + + public IJsonAssertionOptions WhenTypeIs() + where TMemberType : TProperty + { + return (JsonAssertionOptions)restriction.WhenTypeIs(); + } +} diff --git a/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs b/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs new file mode 100644 index 00000000..dc1d764a --- /dev/null +++ b/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs @@ -0,0 +1,103 @@ +using System; +using FluentAssertions.Equivalency; +using FluentAssertions.Primitives; +using Newtonsoft.Json; + +namespace FluentAssertions.Json; + +/// +/// Contains extension methods for JSON serialization assertion methods +/// +public static class ObjectAssertionsExtensions +{ + /// + /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it still retains + /// the values of all members. + /// + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + [CustomAssertion] + public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) + { + return BeJsonSerializable(assertions, options => options, because, becauseArgs); + } + + /// + /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains + /// the values of all members. + /// + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + [CustomAssertion] + public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) + { + return BeJsonSerializable(assertions, options => options, because, becauseArgs); + } + + /// + /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains + /// the values of all members. + /// + /// + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + [CustomAssertion] + public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, Func, EquivalencyOptions> options, string because = "", params object[] becauseArgs) + { + assertions.CurrentAssertionChain.ForCondition(assertions.Subject != null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:object} to be JSON serializable{reason}, but the value is null. Please provide a value for the assertion."); + + assertions.CurrentAssertionChain.ForCondition(assertions.Subject is T) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:object} to be JSON serializable{reason}, but {context:object} is not assignable to {0}", typeof(T)); + + try + { + var deserializedObject = CreateCloneUsingJsonSerializer(assertions.Subject); + + var defaultOptions = AssertionConfiguration.Current.Equivalency.CloneDefaults() + .PreferringRuntimeMemberTypes() + .IncludingFields() + .IncludingProperties(); + + var typedSubject = (T)assertions.Subject; + ((T)deserializedObject).Should().BeEquivalentTo(typedSubject, _ => options(defaultOptions)); + } +#pragma warning disable CA1031 // Ignore catching general exception + catch (Exception exc) +#pragma warning restore CA1031 // Ignore catching general exception + { + assertions.CurrentAssertionChain + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:object} to be JSON serializable{reason}, but serializing {0} failed with {1}", assertions.Subject, exc); + } + + return new AndConstraint(assertions); + } + + private static object CreateCloneUsingJsonSerializer(object subject) + { + var serializedObject = JsonConvert.SerializeObject(subject); + var cloneUsingJsonSerializer = JsonConvert.DeserializeObject(serializedObject, subject.GetType()); + return cloneUsingJsonSerializer; + } +} diff --git a/Src/FluentAssertions.Json/StringAssertionsExtensions.cs b/Src/FluentAssertions.Json/StringAssertionsExtensions.cs index 894a7b98..689dccc5 100644 --- a/Src/FluentAssertions.Json/StringAssertionsExtensions.cs +++ b/Src/FluentAssertions.Json/StringAssertionsExtensions.cs @@ -1,31 +1,31 @@ using System; -using FluentAssertions.Execution; using FluentAssertions.Primitives; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +public static class StringAssertionsExtensions { - public static class StringAssertionsExtensions + [CustomAssertionAttribute] + public static AndWhichConstraint BeValidJson( + this StringAssertions stringAssertions, + string because = "", + params object[] becauseArgs) { - [CustomAssertionAttribute] - public static AndWhichConstraint BeValidJson( - this StringAssertions stringAssertions, - string because = "", - params object[] becauseArgs) - { - JToken json = null; + JToken json = null; - try - { - json = JToken.Parse(stringAssertions.Subject); - } - catch (Exception ex) - { - Execute.Assertion.BecauseOf(because, becauseArgs) - .FailWith("Expected {context:string} to be valid JSON{reason}, but parsing failed with {0}.", ex.Message); - } - - return new AndWhichConstraint(stringAssertions, json); + try + { + json = JToken.Parse(stringAssertions.Subject); } +#pragma warning disable CA1031 // Ignore catching general exception + catch (Exception ex) +#pragma warning restore CA1031 // Ignore catching general exception + { + stringAssertions.CurrentAssertionChain.BecauseOf(because, becauseArgs) + .FailWith("Expected {context:string} to be valid JSON{reason}, but parsing failed with {0}.", ex.Message); + } + + return new AndWhichConstraint(stringAssertions, json); } -} \ No newline at end of file +} diff --git a/Src/FluentAssertions.nuspec b/Src/FluentAssertions.nuspec deleted file mode 100644 index 66fce497..00000000 --- a/Src/FluentAssertions.nuspec +++ /dev/null @@ -1,34 +0,0 @@ - - - - FluentAssertions.Json - 5.0.0 - Codestin Search App - Marcel Körtgen - Dennis Doomen - https://github.com/fluentassertions/fluentassertions.json/blob/master/LICENSE - false - Fluent Assertions extensions for Newtonsoft.Json - Fluent Assertions extensions for Newtonsoft.Json - Copyright Dennis Doomen 2010-2019 - - See https://github.com/fluentassertions/fluentassertions.json/releases/ - - https://www.fluentassertions.com - - - - - - - - - - - - - - - - - diff --git a/Src/FluentAssertions.png b/Src/FluentAssertions.png new file mode 100644 index 00000000..94c749a6 Binary files /dev/null and b/Src/FluentAssertions.png differ diff --git a/Tests/.editorconfig b/Tests/.editorconfig new file mode 100644 index 00000000..5cc57a60 --- /dev/null +++ b/Tests/.editorconfig @@ -0,0 +1,14 @@ +[*.cs] + +# CA1861: Avoid constant arrays as arguments +dotnet_diagnostic.CA1861.severity = none + +# Purpose: Use an overload that has a IEqualityComparer or IComparer parameter +# Reason: Not important in tests +dotnet_diagnostic.MA0002.severity = none + +# File name must match type name. Too many purposeful violations. +dotnet_diagnostic.MA0048.severity = none + +# Mark local variable as const. +dotnet_diagnostic.RCS1118.severity = none \ No newline at end of file diff --git a/Tests/Approval.Tests/ApiApproval.cs b/Tests/Approval.Tests/ApiApproval.cs new file mode 100644 index 00000000..c5563905 --- /dev/null +++ b/Tests/Approval.Tests/ApiApproval.cs @@ -0,0 +1,53 @@ +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System.Xml.Linq; +using System.Xml.XPath; +using PublicApiGenerator; +using VerifyTests; +using VerifyTests.DiffPlex; +using VerifyXunit; +using Xunit; + +namespace Approval.Tests; + +public class ApiApproval +{ + static ApiApproval() => VerifyDiffPlex.Initialize(OutputType.Minimal); + + [Theory] + [ClassData(typeof(TargetFrameworksTheoryData))] + public Task ApproveApi(string framework) + { + var configuration = typeof(ApiApproval).Assembly.GetCustomAttribute()!.Configuration; + var assemblyFile = CombinedPaths("Src", "FluentAssertions.Json", "bin", configuration, framework, "FluentAssertions.Json.dll"); + var assembly = Assembly.LoadFile(assemblyFile); + var publicApi = assembly.GeneratePublicApi(options: null); + + return Verifier + .Verify(publicApi) + .ScrubLinesContaining("FrameworkDisplayName") + .UseDirectory(Path.Combine("ApprovedApi", "FluentAssertions.Json")) + .UseFileName(framework) + .DisableDiff(); + } + + private class TargetFrameworksTheoryData : TheoryData + { + public TargetFrameworksTheoryData() + { + var csproj = CombinedPaths("Src", "FluentAssertions.Json", "FluentAssertions.Json.csproj"); + var project = XDocument.Load(csproj); + var targetFrameworks = project.XPathSelectElement("/Project/PropertyGroup/TargetFrameworks"); + AddRange(targetFrameworks!.Value.Split(';')); + } + } + + private static string GetSolutionDirectory([CallerFilePath] string path = "") => + Path.Combine(Path.GetDirectoryName(path)!, "..", ".."); + + private static string CombinedPaths(params string[] paths) => + Path.GetFullPath(Path.Combine(paths.Prepend(GetSolutionDirectory()).ToArray())); +} diff --git a/Tests/Approval.Tests/Approval.Tests.csproj b/Tests/Approval.Tests/Approval.Tests.csproj new file mode 100644 index 00000000..d9b9ac45 --- /dev/null +++ b/Tests/Approval.Tests/Approval.Tests.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt new file mode 100644 index 00000000..0e2bec32 --- /dev/null +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt @@ -0,0 +1,77 @@ +[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/fluentassertions/fluentassertions.json.git")] +namespace FluentAssertions.Json +{ + public interface IJsonAssertionOptions + { + FluentAssertions.Json.IJsonAssertionRestriction Using(System.Action> action); + FluentAssertions.Json.IJsonAssertionOptions WithoutStrictOrdering(); + } + public interface IJsonAssertionRestriction + { + FluentAssertions.Json.IJsonAssertionOptions WhenTypeIs() + where TMemberType : TMember; + } + public class JTokenAssertions : FluentAssertions.Primitives.ReferenceTypeAssertions + { + public JTokenAssertions(Newtonsoft.Json.Linq.JToken subject, FluentAssertions.Execution.AssertionChain assertionChain) { } + protected override string Identifier { get; } + public FluentAssertions.AndConstraint BeEquivalentTo(Newtonsoft.Json.Linq.JToken expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeEquivalentTo(string expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeEquivalentTo(Newtonsoft.Json.Linq.JToken expected, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(string subtree, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public string Format(Newtonsoft.Json.Linq.JToken value, bool useLineBreaks = false) { } + public FluentAssertions.AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint HaveElement(string expected) { } + public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because, params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveValue(string expected) { } + public FluentAssertions.AndConstraint HaveValue(string expected, string because, params object[] becauseArgs) { } + public FluentAssertions.AndConstraint MatchRegex(string regularExpression) { } + public FluentAssertions.AndConstraint MatchRegex(string regularExpression, string because, params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotBeEquivalentTo(Newtonsoft.Json.Linq.JToken unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint NotHaveElement(string unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotHaveValue(string unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotMatchRegex(string regularExpression, string because = "", params object[] becauseArgs) { } + } + public class JTokenFormatter : FluentAssertions.Formatting.IValueFormatter + { + public JTokenFormatter() { } + public bool CanHandle(object value) { } + public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { } + } + public static class JsonAssertionExtensions + { + public static FluentAssertions.Json.JTokenAssertions Should(this Newtonsoft.Json.Linq.JObject jObject) { } + public static FluentAssertions.Json.JTokenAssertions Should(this Newtonsoft.Json.Linq.JToken jToken) { } + public static FluentAssertions.Json.JTokenAssertions Should(this Newtonsoft.Json.Linq.JValue jValue) { } + } + public sealed class JsonAssertionOptions : FluentAssertions.Equivalency.EquivalencyOptions, FluentAssertions.Json.IJsonAssertionOptions + { + public JsonAssertionOptions(FluentAssertions.Equivalency.EquivalencyOptions equivalencyAssertionOptions) { } + public FluentAssertions.Json.IJsonAssertionRestriction Using(System.Action> action) { } + public FluentAssertions.Json.IJsonAssertionOptions WithoutStrictOrdering() { } + } + public sealed class JsonAssertionRestriction : FluentAssertions.Json.IJsonAssertionRestriction + { + public FluentAssertions.Json.IJsonAssertionOptions WhenTypeIs() + where TMemberType : TProperty { } + } + public static class ObjectAssertionsExtensions + { + [FluentAssertions.CustomAssertion] + public static FluentAssertions.AndConstraint BeJsonSerializable(this FluentAssertions.Primitives.ObjectAssertions assertions, string because = "", params object[] becauseArgs) { } + [FluentAssertions.CustomAssertion] + public static FluentAssertions.AndConstraint BeJsonSerializable(this FluentAssertions.Primitives.ObjectAssertions assertions, string because = "", params object[] becauseArgs) { } + [FluentAssertions.CustomAssertion] + public static FluentAssertions.AndConstraint BeJsonSerializable(this FluentAssertions.Primitives.ObjectAssertions assertions, System.Func, FluentAssertions.Equivalency.EquivalencyOptions> options, string because = "", params object[] becauseArgs) { } + } + public static class StringAssertionsExtensions + { + [FluentAssertions.CustomAssertion] + public static FluentAssertions.AndWhichConstraint BeValidJson(this FluentAssertions.Primitives.StringAssertions stringAssertions, string because = "", params object[] becauseArgs) { } + } +} \ No newline at end of file diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt new file mode 100644 index 00000000..0e2bec32 --- /dev/null +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt @@ -0,0 +1,77 @@ +[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/fluentassertions/fluentassertions.json.git")] +namespace FluentAssertions.Json +{ + public interface IJsonAssertionOptions + { + FluentAssertions.Json.IJsonAssertionRestriction Using(System.Action> action); + FluentAssertions.Json.IJsonAssertionOptions WithoutStrictOrdering(); + } + public interface IJsonAssertionRestriction + { + FluentAssertions.Json.IJsonAssertionOptions WhenTypeIs() + where TMemberType : TMember; + } + public class JTokenAssertions : FluentAssertions.Primitives.ReferenceTypeAssertions + { + public JTokenAssertions(Newtonsoft.Json.Linq.JToken subject, FluentAssertions.Execution.AssertionChain assertionChain) { } + protected override string Identifier { get; } + public FluentAssertions.AndConstraint BeEquivalentTo(Newtonsoft.Json.Linq.JToken expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeEquivalentTo(string expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeEquivalentTo(Newtonsoft.Json.Linq.JToken expected, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(string subtree, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public string Format(Newtonsoft.Json.Linq.JToken value, bool useLineBreaks = false) { } + public FluentAssertions.AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint HaveElement(string expected) { } + public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because, params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveValue(string expected) { } + public FluentAssertions.AndConstraint HaveValue(string expected, string because, params object[] becauseArgs) { } + public FluentAssertions.AndConstraint MatchRegex(string regularExpression) { } + public FluentAssertions.AndConstraint MatchRegex(string regularExpression, string because, params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotBeEquivalentTo(Newtonsoft.Json.Linq.JToken unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint NotHaveElement(string unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotHaveValue(string unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotMatchRegex(string regularExpression, string because = "", params object[] becauseArgs) { } + } + public class JTokenFormatter : FluentAssertions.Formatting.IValueFormatter + { + public JTokenFormatter() { } + public bool CanHandle(object value) { } + public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { } + } + public static class JsonAssertionExtensions + { + public static FluentAssertions.Json.JTokenAssertions Should(this Newtonsoft.Json.Linq.JObject jObject) { } + public static FluentAssertions.Json.JTokenAssertions Should(this Newtonsoft.Json.Linq.JToken jToken) { } + public static FluentAssertions.Json.JTokenAssertions Should(this Newtonsoft.Json.Linq.JValue jValue) { } + } + public sealed class JsonAssertionOptions : FluentAssertions.Equivalency.EquivalencyOptions, FluentAssertions.Json.IJsonAssertionOptions + { + public JsonAssertionOptions(FluentAssertions.Equivalency.EquivalencyOptions equivalencyAssertionOptions) { } + public FluentAssertions.Json.IJsonAssertionRestriction Using(System.Action> action) { } + public FluentAssertions.Json.IJsonAssertionOptions WithoutStrictOrdering() { } + } + public sealed class JsonAssertionRestriction : FluentAssertions.Json.IJsonAssertionRestriction + { + public FluentAssertions.Json.IJsonAssertionOptions WhenTypeIs() + where TMemberType : TProperty { } + } + public static class ObjectAssertionsExtensions + { + [FluentAssertions.CustomAssertion] + public static FluentAssertions.AndConstraint BeJsonSerializable(this FluentAssertions.Primitives.ObjectAssertions assertions, string because = "", params object[] becauseArgs) { } + [FluentAssertions.CustomAssertion] + public static FluentAssertions.AndConstraint BeJsonSerializable(this FluentAssertions.Primitives.ObjectAssertions assertions, string because = "", params object[] becauseArgs) { } + [FluentAssertions.CustomAssertion] + public static FluentAssertions.AndConstraint BeJsonSerializable(this FluentAssertions.Primitives.ObjectAssertions assertions, System.Func, FluentAssertions.Equivalency.EquivalencyOptions> options, string because = "", params object[] becauseArgs) { } + } + public static class StringAssertionsExtensions + { + [FluentAssertions.CustomAssertion] + public static FluentAssertions.AndWhichConstraint BeValidJson(this FluentAssertions.Primitives.StringAssertions stringAssertions, string because = "", params object[] becauseArgs) { } + } +} \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Net45.Specs/Json.Net45.Specs.csproj b/Tests/FluentAssertions.Json.Net45.Specs/Json.Net45.Specs.csproj deleted file mode 100644 index 4f33d458..00000000 --- a/Tests/FluentAssertions.Json.Net45.Specs/Json.Net45.Specs.csproj +++ /dev/null @@ -1,104 +0,0 @@ - - - - - Debug - AnyCPU - {63A1554C-DB57-463A-9DF4-2748EB403DD9} - Library - Properties - FluentAssertions.Json.Net45.Specs - FluentAssertions.Json.Net45.Specs - v4.5.1 - 512 - - - - ..\..\Src\FluentAssertions.snk - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - false - - - - ..\..\packages\FluentAssertions.5.0.0\lib\net45\FluentAssertions.dll - - - ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - ..\..\packages\System.Runtime.InteropServices.RuntimeInformation.4.0.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll - - - ..\..\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll - - - - - - - - - ..\..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll - - - ..\..\packages\xunit.assert.2.2.0\lib\netstandard1.1\xunit.assert.dll - - - ..\..\packages\xunit.extensibility.core.2.2.0\lib\netstandard1.1\xunit.core.dll - - - ..\..\packages\xunit.extensibility.execution.2.2.0\lib\netstandard1.1\xunit.execution.dotnet.dll - - - - - - - - FluentAssertions.snk - - - - - - - - - {c1d6c0eb-a488-4166-a313-f024c5b6df5a} - FluentAssertions.Json - - - - - - - \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Net45.Specs/Json.Net45.Specs.v2.ncrunchproject b/Tests/FluentAssertions.Json.Net45.Specs/Json.Net45.Specs.v2.ncrunchproject deleted file mode 100644 index 30815b19..00000000 --- a/Tests/FluentAssertions.Json.Net45.Specs/Json.Net45.Specs.v2.ncrunchproject +++ /dev/null @@ -1,26 +0,0 @@ - - true - 1000 - false - false - false - true - false - false - false - false - false - true - true - false - true - true - true - 60000 - - - - AutoDetect - STA - x86 - \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Net45.Specs/Properties/AssemblyInfo.cs b/Tests/FluentAssertions.Json.Net45.Specs/Properties/AssemblyInfo.cs deleted file mode 100644 index d98bceb4..00000000 --- a/Tests/FluentAssertions.Json.Net45.Specs/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("FluentAssertions.Json.Net45.Specs")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("FluentAssertions.Json.Net45.Specs")] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("63a1554c-db57-463a-9df4-2748eb403dd9")] - -[assembly: AssemblyVersion("4.20.0.0")] -[assembly: AssemblyInformationalVersion("4.20.0+Branch.release-5.0.Sha.0619a4b0d933d605ba1f6f5c2a883f516ba9985e")] -[assembly: AssemblyFileVersion("4.20.0.0")] diff --git a/Tests/FluentAssertions.Json.Net45.Specs/packages.config b/Tests/FluentAssertions.Json.Net45.Specs/packages.config deleted file mode 100644 index 4565c198..00000000 --- a/Tests/FluentAssertions.Json.Net45.Specs/packages.config +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Shared.Specs/JTokenAssertionsSpecs.cs b/Tests/FluentAssertions.Json.Shared.Specs/JTokenAssertionsSpecs.cs deleted file mode 100644 index 7618625e..00000000 --- a/Tests/FluentAssertions.Json.Shared.Specs/JTokenAssertionsSpecs.cs +++ /dev/null @@ -1,1096 +0,0 @@ -using System; -using System.Collections.Generic; -using FluentAssertions.Formatting; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Xunit; -using Xunit.Sdk; - -namespace FluentAssertions.Json -{ - // ReSharper disable InconsistentNaming - // ReSharper disable ExpressionIsAlwaysNull - public class JTokenAssertionsSpecs - { - #region (Not)BeEquivalentTo - - [Fact] - public void When_both_objects_are_null_BeEquivalentTo_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - JToken actual = null; - JToken expected = null; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().BeEquivalentTo(expected); - } - - [Fact] - public void When_both_objects_are_the_same_or_equal_BeEquivalentTo_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - string json = @" - { - friends: - [{ - id: 123, - name: ""John Doe"" - }, { - id: 456, - name: ""Jane Doe"", - kids: - [ - ""Jimmy"", - ""James"" - ] - } - ] - } - "; - - var a = JToken.Parse(json); - var b = JToken.Parse(json); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - a.Should().BeEquivalentTo(a); - b.Should().BeEquivalentTo(b); - a.Should().BeEquivalentTo(b); - } - - [Fact] - public void When_objects_differ_BeEquivalentTo_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var testCases = new [] - { - Tuple.Create( - (string)null, - "{ id: 2 }", - "is null") - , - Tuple.Create( - "{ id: 1 }", - (string)null, - "is not null") - , - Tuple.Create( - "{ items: [] }", - "{ items: 2 }", - "has a different type at $.items") - , - Tuple.Create( - "{ items: [ \"fork\", \"knife\" , \"spoon\" ]}", - "{ items: [ \"fork\", \"knife\" ]}", - "has a different length at $.items") - , - Tuple.Create( - "{ items: [ \"fork\", \"knife\" ]}", - "{ items: [ \"fork\", \"knife\" , \"spoon\" ]}", - "has a different length at $.items") - , - Tuple.Create( - "{ items: [ \"fork\", \"knife\" , \"spoon\" ]}", - "{ items: [ \"fork\", \"spoon\", \"knife\" ]}", - "has a different value at $.items[1]") - , - Tuple.Create( - "{ tree: { } }", - "{ tree: \"oak\" }", - "has a different type at $.tree") - , - Tuple.Create( - "{ tree: { leaves: 10} }", - "{ tree: { branches: 5, leaves: 10 } }", - "misses property $.tree.branches") - , - Tuple.Create( - "{ tree: { branches: 5, leaves: 10 } }", - "{ tree: { leaves: 10} }", - "has extra property $.tree.branches") - , - Tuple.Create( - "{ tree: { leaves: 5 } }", - "{ tree: { leaves: 10} }", - "has a different value at $.tree.leaves") - , - Tuple.Create( - "{ eyes: \"blue\" }", - "{ eyes: [] }", - "has a different type at $.eyes") - , - Tuple.Create( - "{ eyes: \"blue\" }", - "{ eyes: 2 }", - "has a different type at $.eyes") - , - Tuple.Create( - "{ id: 1 }", - "{ id: 2 }", - "has a different value at $.id") - }; - - foreach (var testCase in testCases) - { - string actualJson = testCase.Item1; - string expectedJson = testCase.Item2; - string expectedDifference = testCase.Item3; - - var actual = (actualJson != null) ? JToken.Parse(actualJson) : null; - var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null; - - var expectedMessage = - $"JSON document {expectedDifference}." + - $"Expected" + - $"{Format(actual, true)}" + - $"to be equivalent to" + - $"{Format(expected, true)}."; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().Invoking(x => x.BeEquivalentTo(expected)) - .Should().Throw() - .WithMessage(expectedMessage); - } - } - - [Fact] - public void When_properties_differ_BeEquivalentTo_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var testCases = new [] - { - Tuple.Create( - new JProperty("eyes", "blue"), - new JArray(), - "has a different type at $") - , - Tuple.Create( - new JProperty("eyes", "blue"), - new JProperty("hair", "black"), - "has a different name at $") - , - }; - - foreach (var testCase in testCases) - { - var actual = testCase.Item1; - var expected = testCase.Item2; - var expectedDifference = testCase.Item3; - - var expectedMessage = - $"JSON document {expectedDifference}." + - $"Expected" + - $"{Format(actual, true)}" + - $"to be equivalent to" + - $"{Format(expected, true)}."; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().Invoking(x => x.BeEquivalentTo(expected)) - .Should().Throw() - .WithMessage(expectedMessage); - } - } - - [Fact] - public void When_both_properties_are_null_BeEquivalentTo_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var actual = JToken.Parse("{ \"id\": null }"); - var expected = JToken.Parse("{ \"id\": null }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().BeEquivalentTo(expected); - } - - [Fact] - public void When_arrays_are_equal_BeEquivalentTo_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var testCases = new [] - { - Tuple.Create( - new JArray(1, 2, 3), - new JArray(1, 2, 3)) - , - Tuple.Create( - new JArray("blue", "green"), - new JArray("blue", "green")) - , - Tuple.Create( - new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")), - new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}"))) - }; - - foreach (var testCase in testCases) - { - var actual = testCase.Item1; - var expected = testCase.Item2; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().BeEquivalentTo(expected); - } - } - - [Fact] - public void When_only_the_order_of_properties_differ_BeEquivalentTo_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var testCases = new Dictionary - { - { - "{ friends: [{ id: 123, name: \"Corby Page\" }, { id: 456, name: \"Carter Page\" }] }", - "{ friends: [{ name: \"Corby Page\", id: 123 }, { id: 456, name: \"Carter Page\" }] }" - }, - { - "{ id: 2, admin: true }", - "{ admin: true, id: 2}" - } - }; - - foreach (var testCase in testCases) - { - var actualJson = testCase.Key; - var expectedJson = testCase.Value; - var a = JToken.Parse(actualJson); - var b = JToken.Parse(expectedJson); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - a.Should().BeEquivalentTo(b); - } - } - - [Fact] - public void When_checking_whether_a_JToken_is_equivalent_to_the_string_representation_of_that_token_it_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - string jsonString = @" - { - friends: - [{ - id: 123, - name: ""John Doe"" - }, { - id: 456, - name: ""Jane Doe"", - kids: - [ - ""Jimmy"", - ""James"" - ] - } - ] - } - "; - - var actualJSON = JToken.Parse(jsonString); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actualJSON.Should().BeEquivalentTo(jsonString); - } - - [Fact] - public void When_checking_equivalency_with_an_invalid_expected_string_it_should_provide_a_clear_error_message() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var actualJson = JToken.Parse("{ \"id\": null }"); - var expectedString = "{ invalid JSON }"; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actualJson.Should().Invoking(x => x.BeEquivalentTo(expectedString)) - .Should().Throw() - .WithMessage($"Unable to parse expected JSON string:{expectedString}*") - .WithInnerException(); - } - - [Fact] - public void When_checking_non_equivalency_with_an_invalid_unexpected_string_it_should_provide_a_clear_error_message() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var actualJson = JToken.Parse("{ \"id\": null }"); - var unexpectedString = "{ invalid JSON }"; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actualJson.Should().Invoking(x => x.NotBeEquivalentTo(unexpectedString)) - .Should().Throw() - .WithMessage($"Unable to parse unexpected JSON string:{unexpectedString}*") - .WithInnerException(); - } - - [Fact] - public void When_specifying_a_reason_why_object_should_be_equivalent_it_should_use_that_in_the_error_message() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ child: { subject: 'foo' } }"); - var expected = JToken.Parse("{ child: { expected: 'bar' } }"); - - var expectedMessage = - $"JSON document misses property $.child.expected." + - $"Expected" + - $"{Format(subject, true)}" + - $"to be equivalent to" + - $"{Format(expected, true)} " + - "because we want to test the failure message."; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject.Should().Invoking(x => x.BeEquivalentTo(expected, "we want to test the failure {0}", "message")) - .Should().Throw() - .WithMessage(expectedMessage); - } - - [Fact] - public void When_objects_differ_NotBeEquivalentTo_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var actual = JToken.Parse("{ \"id\": 1 }"); - var expected = JToken.Parse("{ \"id\": 2 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().NotBeEquivalentTo(expected); - } - - [Fact] - public void When_objects_are_equal_NotBeEquivalentTo_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var a = JToken.Parse("{ \"id\": 1 }"); - var b = JToken.Parse("{ \"id\": 1 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - a.Invoking(x => x.Should().NotBeEquivalentTo(b)) - .Should().Throw() - .WithMessage($"Expected JSON document not to be equivalent to {Format(b)}."); - } - - [Fact] - public void When_checking_whether_a_JToken_is_not_equivalent_to_the_string_representation_of_that_token_it_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - string jsonString = "{ \"id\": 1 }"; - var actualJson = JToken.Parse(jsonString); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action action = () => actualJson.Should().NotBeEquivalentTo(jsonString); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - action.Should().Throw() - .WithMessage("Expected JSON document not to be equivalent*"); - } - - #endregion (Not)BeEquivalentTo - - #region (Not)HaveValue - - [Fact] - public void When_jtoken_has_value_HaveValue_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject["id"].Should().HaveValue("42"); - } - - [Fact] - public void When_jtoken_not_has_value_HaveValue_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject["id"].Should().Invoking(x => x.HaveValue("43", "because foo")) - .Should().Throw() - .WithMessage("Expected JSON property \"id\" to have value \"43\" because foo, but found \"42\"."); - } - - [Fact] - public void When_jtoken_does_not_have_value_NotHaveValue_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 43 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject["id"].Should().NotHaveValue("42"); - } - - [Fact] - public void When_jtoken_does_have_value_NotHaveValue_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject["id"].Should().Invoking(x => x.NotHaveValue("42", "because foo")) - .Should().Throw() - .WithMessage("Did not expect JSON property \"id\" to have value \"42\" because foo."); - } - - #endregion (Not)HaveValue - - #region (Not)MatchRegex - - [Fact] - public void When_json_matches_regex_pattern_MatchRegex_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject["id"].Should().MatchRegex("\\d{2}"); - } - - [Fact] - public void When_json_does_not_match_regex_pattern_MatchRegex_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 'not two digits' }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject["id"].Should().Invoking(x => x.MatchRegex("\\d{2}", "because foo")) - .Should().Throw() - .WithMessage("Expected JSON property \"id\" to match regex pattern \"\\d{2}\" because foo, but found \"not two digits\"."); - } - - [Fact] - public void When_json_does_not_match_regex_pattern_NotHaveRegexValue_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 'not two digits' }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject["id"].Should().NotMatchRegex("\\d{2}"); - } - - [Fact] - public void When_json_matches_regex_pattern_NotHaveRegexValue_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject["id"].Should().Invoking(x => x.NotMatchRegex("\\d{2}", "because foo")) - .Should().Throw() - .WithMessage("Did not expect JSON property \"id\" to match regex pattern \"\\d{2}\" because foo."); - } - - #endregion (Not)MatchRegex - - #region (Not)HaveElement - - [Fact] - public void When_jtoken_has_element_HaveElement_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject.Should().HaveElement("id"); - } - - [Fact] - public void When_jtoken_not_has_element_HaveElement_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject.Should().Invoking(x => x.HaveElement("name", "because foo")) - .Should().Throw() - .WithMessage($"Expected JSON document {Format(subject)} to have element \"name\" because foo, but no such element was found."); - } - - [Fact] - public void When_jtoken_does_not_have_element_NotHaveElement_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject.Should().NotHaveElement("name"); - } - - [Fact] - public void When_jtoken_does_have_element_NotHaveElement_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ 'id': 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - subject.Should().Invoking(x => x.NotHaveElement("id", "because foo")) - .Should().Throw() - .WithMessage($"Did not expect JSON document {Format(subject)} to have element \"id\" because foo."); - } - - #endregion (Not)HaveElement - - #region ContainSingleItem - - [Fact] - public void When_jtoken_has_a_single_element_ContainSingleItem_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ id: 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSingleItem(); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().NotThrow(); - } - - [Fact] - public void When_jtoken_has_a_single_element_ContainSingleItem_should_return_which_element_it_is() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ id: 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - var element = subject.Should().ContainSingleItem().Which; - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - element.Should().BeEquivalentTo(new JProperty("id", 42)); - } - - [Fact] - public void When_jtoken_is_null_ContainSingleItem_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - JToken subject = null; - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSingleItem("null is not allowed"); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw() - .WithMessage("Expected JSON document to contain a single item because null is not allowed, but found ."); - } - - [Fact] - public void When_jtoken_is_an_empty_object_ContainSingleItem_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ }"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSingleItem("less is not allowed"); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw() - .WithMessage("Expected JSON document * to contain a single item because less is not allowed, but the collection is empty."); - } - - [Fact] - public void When_jtoken_has_multiple_elements_ContainSingleItem_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ id: 42, admin: true }"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSingleItem("more is not allowed"); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw() - .WithMessage("Expected JSON document*id*42*admin*true*to contain a single item because more is not allowed, but found*"); - } - - [Fact] - public void When_jtoken_is_array_with_a_single_item_ContainSingleItem_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("[{ id: 42 }]"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSingleItem(); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().NotThrow(); - } - - [Fact] - public void When_jtoken_is_an_array_with_a_single_item_ContainSingleItem_should_return_which_element_it_is() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("[{ id: 42 }]"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - var element = subject.Should().ContainSingleItem().Which; - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - element.Should().BeEquivalentTo(JToken.Parse("{ id: 42 }")); - } - - [Fact] - public void When_jtoken_is_an_empty_array_ContainSingleItem_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("[]"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSingleItem("less is not allowed"); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw() - .WithMessage("Expected JSON document [] to contain a single item because less is not allowed, but the collection is empty."); - } - - [Fact] - public void When_jtoken_is_an_array_with_multiple_items_ContainSingleItem_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("[1, 2]"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSingleItem("more is not allowed"); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - string formattedSubject = Format(subject); - - act.Should().Throw() - .WithMessage($"Expected JSON document {formattedSubject} to contain a single item because more is not allowed, but found {formattedSubject}."); - } - - #endregion ContainSingleItem - - #region HaveCount - - [Fact] - public void When_expecting_the_actual_number_of_elements_HaveCount_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ id: 42, admin: true }"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().HaveCount(2); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().NotThrow(); - } - - [Fact] - public void When_expecting_the_actual_number_of_elements_HaveCount_should_enable_consecutive_assertions() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ id: 42 }"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - JTokenAssertions and = subject.Should().HaveCount(1).And; - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - and.BeEquivalentTo(subject); - } - - [Fact] - public void When_jtoken_is_null_HaveCount_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - JToken subject = null; - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().HaveCount(1, "null is not allowed"); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw() - .WithMessage("Expected JSON document to contain 1 item(s) because null is not allowed, but found ."); - } - - [Fact] - public void When_expecting_a_different_number_of_elements_than_the_actual_number_HaveCount_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ }"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().HaveCount(1, "numbers matter"); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw() - .WithMessage("Expected JSON document * to contain 1 item(s) because numbers matter, but found 0."); - } - - [Fact] - public void When_expecting_the_actual_number_of_array_items_HaveCount_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("[ 'Hello', 'World!' ]"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().HaveCount(2); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().NotThrow(); - } - - [Fact] - public void When_expecting_a_different_number_of_array_items_than_the_actual_number_HaveCount_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("[ 'Hello', 'World!' ]"); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().HaveCount(3, "the more the better"); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw() - .WithMessage("Expected JSON document * to contain 3 item(s) because the more the better, but found 2."); - } - - #endregion HaveCount - - #region ContainSubtree - - [Fact] - public void When_all_expected_subtree_properties_match_ContainSubtree_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', baz: 'baz'} "); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSubtree(" { foo: 'foo', baz: 'baz' } "); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().NotThrow(); - } - - [Fact] - public void When_subtree_properties_are_missing_ContainSubtree_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar' } "); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSubtree(" { baz: 'baz' } "); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw(); - } - - [Fact] - public void When_deep_subtree_matches_ContainSubtree_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } }} "); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSubtree(" { child: { grandchild: { tag: 'abrakadabra' } } } "); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().NotThrow(); - } - - [Fact] - public void When_deep_subtree_does_not_match_ContainSubtree_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } }} "); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSubtree(" { child: { grandchild: { tag: 'ooops' } } } "); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw(); - } - - [Fact] - public void When_array_elements_are_matching_ContainSubtree_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', items: [ { id: 1 }, { id: 2 }, { id: 3 } ] } "); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSubtree(" { items: [ { id: 1 }, { id: 3 } ] } "); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().NotThrow(); - } - - [Fact] - public void When_array_elements_are_missing_ContainSubtree_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', items: [ { id: 1 }, { id: 3 }, { id: 5 } ] } "); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSubtree(" { items: [ { id: 1 }, { id: 2 } ] } "); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw(); - } - - [Fact] - public void When_property_types_dont_match_ContainSubtree_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var subject = JToken.Parse("{ foo: '1' } "); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().ContainSubtree(" { foo: 1 } "); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().Throw(); - } - - [Fact] - public void When_checking_subtree_with_an_invalid_expected_string_it_should_provide_a_clear_error_message() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var actualJson = JToken.Parse("{ \"id\": null }"); - var invalidSubtree = "{ invalid JSON }"; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - actualJson.Should().Invoking(x => x.ContainSubtree(invalidSubtree)) - .Should().Throw() - .WithMessage($"Unable to parse expected JSON string:{invalidSubtree}*") - .WithInnerException(); - } - - #endregion - - private static string Format(JToken value, bool useLineBreaks = false) - { - return new JTokenFormatter().Format(value, new FormattingContext - { - UseLineBreaks = useLineBreaks - }, null); - } - } -} \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Shared.Specs/JTokenFormatterSpecs.cs b/Tests/FluentAssertions.Json.Shared.Specs/JTokenFormatterSpecs.cs deleted file mode 100644 index b57d8453..00000000 --- a/Tests/FluentAssertions.Json.Shared.Specs/JTokenFormatterSpecs.cs +++ /dev/null @@ -1,94 +0,0 @@ -using FluentAssertions.Formatting; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace FluentAssertions.Json -{ - // ReSharper disable InconsistentNaming - public class JTokenFormatterSpecs - { - [Fact] - public void Should_Handle_JToken() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var sut = new JTokenFormatter(); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - var actual = sut.CanHandle(JToken.Parse("{}")); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().BeTrue(); - } - - [Fact] - public void Should_not_handle_anything_else() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var testCases = new object[] { null, string.Empty }; - var sut = new JTokenFormatter(); - - foreach (var testCase in testCases) - { - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - var actual = sut.CanHandle(testCase); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().BeFalse(); - } - } - - - [Fact] - public void Should_preserve_indenting() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var json = JToken.Parse("{ \"id\":1 }"); - var sut = new JTokenFormatter(); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - var actual = sut.Format(json, new FormattingContext{UseLineBreaks = true}, (path, value) => ""); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().Be(json.ToString(Newtonsoft.Json.Formatting.Indented)); - } - - [Fact] - public void Should_Remove_line_breaks_and_indenting() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var json = JToken.Parse("{ \"id\":1 }"); - var sut = new JTokenFormatter(); - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - // ReSharper disable once RedundantArgumentDefaultValue - var actual = sut.Format(json, new FormattingContext{UseLineBreaks = false}, (path, value) => ""); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - actual.Should().Be(json.ToString().RemoveNewLines()); - } - } -} \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Shared.Specs/Json.Shared.Specs.shproj b/Tests/FluentAssertions.Json.Shared.Specs/Json.Shared.Specs.shproj deleted file mode 100644 index 7412852f..00000000 --- a/Tests/FluentAssertions.Json.Shared.Specs/Json.Shared.Specs.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 845ed06b-1f6d-4442-bccc-52483561f447 - 14.0 - - - - - - - - diff --git a/Tests/FluentAssertions.Json.Shared.Specs/JsonAssertionExtensionsSpecs.cs b/Tests/FluentAssertions.Json.Shared.Specs/JsonAssertionExtensionsSpecs.cs deleted file mode 100644 index 376027de..00000000 --- a/Tests/FluentAssertions.Json.Shared.Specs/JsonAssertionExtensionsSpecs.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Newtonsoft.Json.Linq; -using FluentAssertions; -using FluentAssertions.Json; -using Xunit; - -// NOTE that we are using both namespaces 'FluentAssertions' & 'FluentAssertions.Json' from an external namespace to force compiler disambiguation warnings -namespace SomeOtherNamespace -{ - // ReSharper disable InconsistentNaming - public class JsonAssertionExtensionsSpecs - { - [Fact] - public void Should_Provide_Unambiguos_JTokenAssertions() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - var assertions = new [] - { - JToken.Parse("{\"token\":\"value\"}").Should() - , new JProperty("property","value").Should() - , new JObject(new JProperty("object", "value")).Should() - , new JArray(new [] { 42, 43}).Should() - , new JConstructor("property","value").Should() - , new JValue("value").Should() - , new JRaw("value").Should() - }; - - //----------------------------------------------------------------------------------------------------------- - // Act & Assert - //----------------------------------------------------------------------------------------------------------- - foreach(var sut in assertions) - sut.Should().BeOfType("extensions should provide assertions for all JSon primitives, i.e. JObject, JToken and JProperty"); - } - } -} \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Shared.Specs/Shared.Json.Specs.projitems b/Tests/FluentAssertions.Json.Shared.Specs/Shared.Json.Specs.projitems deleted file mode 100644 index 77f5eee4..00000000 --- a/Tests/FluentAssertions.Json.Shared.Specs/Shared.Json.Specs.projitems +++ /dev/null @@ -1,18 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 845ed06b-1f6d-4442-bccc-52483561f447 - - - Shared.Json.Specs - - - - - - - - - \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Shared.Specs/StringAssertionsExtensionsSpecs.cs b/Tests/FluentAssertions.Json.Shared.Specs/StringAssertionsExtensionsSpecs.cs deleted file mode 100644 index bdd2cd57..00000000 --- a/Tests/FluentAssertions.Json.Shared.Specs/StringAssertionsExtensionsSpecs.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using Newtonsoft.Json.Linq; -using Xunit; -using Xunit.Sdk; - -namespace FluentAssertions.Json.Net45.Specs -{ - // ReSharper disable ExpressionIsAlwaysNull - public class StringAssertionsExtensionsSpecs - { - #region BeValidJson - - [Fact] - public void When_checking_valid_json_BeValidJson_should_succeed() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - string subject = "{ id: 42, admin: true }"; - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - Action act = () => subject.Should().BeValidJson(); - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - act.Should().NotThrow(); - } - - [Fact] - public void When_checking_valid_json_BeValidJson_should_enable_consecutive_jtoken_assertions() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - string subject = "{ id: 42 }"; - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - object which = subject.Should().BeValidJson().Which; - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - which.Should().BeAssignableTo(); - } - - [Fact] - public void When_checking_null_BeValidJson_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - string subject = null; - Exception caughtException = null; - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - try - { - subject.Should().BeValidJson("null is not allowed"); - } - catch (Exception ex) - { - caughtException = ex; - } - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - caughtException.Should() - .BeOfType() - .Which.Message.Should() - .Match("Expected subject to be valid JSON because null is not allowed, but parsing failed with \"*\"."); - } - - [Fact] - public void When_checking_invalid_json_BeValidJson_should_fail() - { - //----------------------------------------------------------------------------------------------------------- - // Arrange - //----------------------------------------------------------------------------------------------------------- - string subject = "invalid json"; - Exception caughtException = null; - - //----------------------------------------------------------------------------------------------------------- - // Act - //----------------------------------------------------------------------------------------------------------- - try - { - subject.Should().BeValidJson("we like {0}", "JSON"); - } - catch (Exception ex) - { - caughtException = ex; - } - - //----------------------------------------------------------------------------------------------------------- - // Assert - //----------------------------------------------------------------------------------------------------------- - caughtException.Should() - .BeOfType() - .Which.Message.Should() - .Match("Expected subject to be valid JSON because we like JSON, but parsing failed with \"*\"."); - } - - #endregion - - } -} \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Shared.Specs/StringExtensions.cs b/Tests/FluentAssertions.Json.Shared.Specs/StringExtensions.cs deleted file mode 100644 index 4dfe7f8e..00000000 --- a/Tests/FluentAssertions.Json.Shared.Specs/StringExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace FluentAssertions.Json -{ - internal static class StringExtensions - { - public static string RemoveNewLines(this string @this) - { - return @this.Replace("\n", "").Replace("\r", "").Replace("\\r\\n", ""); - } - } -} \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Specs/.editorconfig b/Tests/FluentAssertions.Json.Specs/.editorconfig new file mode 100644 index 00000000..436717d4 --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/.editorconfig @@ -0,0 +1,141 @@ +[*.cs] + +# IDE0051: Private member is unused +dotnet_diagnostic.IDE0051.severity = none +# IDE0070: GetHashCode implementation can be simplified +dotnet_diagnostic.IDE0070.severity = none + +# CA1002 Change 'List' to use 'Collection', 'ReadOnlyCollection' or 'KeyedCollection' +dotnet_diagnostic.CA1002.severity = none +# CA1003 Change the event to replace the type with a generic EventHandler +dotnet_diagnostic.CA1003.severity = none +# CA1008: Add a member to Color that has a value of zero with a suggested name of 'None' +dotnet_diagnostic.CA1008.severity = none +# CA1014 Mark assemblies with CLSCompliant +dotnet_diagnostic.CA1014.severity = none +# CA1017 Mark assembly with ComVisisble(false) +dotnet_diagnostic.CA1017.severity = none +# CA1024 Use properties where appropriate +dotnet_diagnostic.CA1024.severity = none +# CA1028: Enum Storage should be Int32 +dotnet_diagnostic.CA1028.severity = none +# CA1032: Implement standard exception constructors +dotnet_diagnostic.CA1032.severity = none +# CA1036: Override methods on comparable types +dotnet_diagnostic.CA1036.severity = none +# CA1040: Avoid empty interfaces +dotnet_diagnostic.CA1040.severity = none +# CA1044: Properties should not be write only +dotnet_diagnostic.CA1044.severity = none +# CA1051: Do not declare visible instance fields +dotnet_diagnostic.CA1051.severity = none +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = none +# CA1064: Exceptions should be public +dotnet_diagnostic.CA1064.severity = none +# CA1307: Specify StringComparison +dotnet_diagnostic.CA1307.severity = none +# CA1506 Rewrite or refactor the code to decrease its class coupling +dotnet_diagnostic.CA1506.severity = none +# CA1707: Remove the underscores from member name +dotnet_diagnostic.CA1707.severity = none +# CA1711: Rename type name so that it does not end in 'Enum' +dotnet_diagnostic.CA1711.severity = none +# CA1714: Flags enums should have plural names +dotnet_diagnostic.CA1714.severity = none +# CA1716: Identifiers should not match keywords +dotnet_diagnostic.CA1716.severity = none +# CA1813 Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = none +# CA1814: Prefer jagged arrays over multidimensional +dotnet_diagnostic.CA1814.severity = none +# CA1818: Type is an internal class that is apparently never instantiated. +dotnet_diagnostic.CA1812.severity = none +# CA1822: Member does not access instance data and can be marked as static +dotnet_diagnostic.CA1822.severity = none +# CA2000: Dispose objects before losing scope +dotnet_diagnostic.CA2000.severity = none +# CA2201: Exception type System.Exception is not sufficiently specific +dotnet_diagnostic.CA2201.severity = none +# CA2208: Call the ArgumentNullException constructor that contains a message and/or paramName parameter +dotnet_diagnostic.CA2208.severity = none +# CA2227: Collection properties should be read only +dotnet_diagnostic.CA2227.severity = none +# CA5394 Random is an insecure random number generator +dotnet_diagnostic.CA5394.severity = none + +# AV1000: Type contains the word 'and', which suggests it has multiple purposes +dotnet_diagnostic.AV1000.severity = none +# AV1008: Class should be non-static or its name should be suffixed with Extensions +dotnet_diagnostic.AV1008.severity = none +# AV1115: Member or local function contains the word 'and', which suggests doing multiple things +dotnet_diagnostic.AV1115.severity = none +# AV1135: Do not return null for strings, collections or tasks +dotnet_diagnostic.AV1135.severity = none +# AV1505: Namespace should match with assembly name +dotnet_diagnostic.AV1505.severity = none +# AV1564: Parameter in public or internal member is of type bool or bool? +dotnet_diagnostic.AV1564.severity = none +# AV1708: Type name contains term that should be avoided +dotnet_diagnostic.AV1708.severity = none +# AV1225: Method raises event, so it should be named +dotnet_diagnostic.AV1225.severity = none +# AV1250; Method returns the result of a query, which uses deferred execution +dotnet_diagnostic.AV1250.severity = none +# AV1507: File contains additional type +dotnet_diagnostic.AV1507.severity = none +# AV1532: Loop statement contains nested loop +dotnet_diagnostic.AV1532.severity = none +# AV1555: Parameter in the call to is invoked with a named argument +dotnet_diagnostic.AV1555.severity = none +# AV1704: Type contains one or more digits in its name +dotnet_diagnostic.AV1704.severity = none +# AV1706: Parameter should have a more descriptive name +dotnet_diagnostic.AV1706.severity = none +# AV1710: Property contains the name of its containing type +dotnet_diagnostic.AV1710.severity = none +# AV1755: Name of async method should end with Async or TaskAsync +dotnet_diagnostic.AV1755.severity = none + +# SA0001: XmlCommentAnalysisDisabled +dotnet_diagnostic.SA0001.severity = none +# SA1001: CommasMustBeSpacedCorrectly +dotnet_diagnostic.SA1001.severity = none +# SA1009: Closing parenthesis should not be preceded by a space +dotnet_diagnostic.SA1009.severity = none +# SA1111: Closing parenthesis should be on line of the last parameter +dotnet_diagnostic.SA1111.severity = none +# SA1118: The parameter spans multiple lines +dotnet_diagnostic.SA1118.severity = none +# SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none +# SA1124: Regions should not be used +dotnet_diagnostic.SA1124.severity = none +# SA1312: variable should begin with lower-case letter +dotnet_diagnostic.SA1312.severity = none # re-enable if using statements can be discarded +# SA1313: parameter should begin with lower-case letter +dotnet_diagnostic.SA1313.severity = none # re-enable when parameters discards are available +# SA1401: Field should be private +dotnet_diagnostic.SA1401.severity = none +# SA1402: File may only contain a single type +dotnet_diagnostic.SA1402.severity = none +# SA1403: File may only contain a single namespace +dotnet_diagnostic.SA1403.severity = none +# SA1404: Remove unused locals +dotnet_diagnostic.SA1404.severity = none +# SA1502: Element should not be on a single line +dotnet_diagnostic.SA1502.severity = none +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = none +# SA1611: The documentation for parameter is missing +dotnet_diagnostic.SA1611.severity = none +# SA1615: Element return value should be documented +dotnet_diagnostic.SA1615.severity = none + +# SA1005: Single line comments should begin with single space +dotnet_diagnostic.SA1005.severity = suggestion + +# ReSharper/Rider +resharper_expression_is_always_null_highlighting=none diff --git a/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj new file mode 100644 index 00000000..e8e585bf --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj @@ -0,0 +1,25 @@ + + + false + net47;net8.0 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs new file mode 100644 index 00000000..303788fa --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs @@ -0,0 +1,992 @@ +using System; +using FluentAssertions.Formatting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Json.Specs; + +// ReSharper disable InconsistentNaming +// ReSharper disable ExpressionIsAlwaysNull +public class JTokenAssertionsSpecs +{ + #region (Not)BeEquivalentTo + + [Fact] + public void When_both_tokens_are_null_they_should_be_treated_as_equivalent() + { + // Arrange + JToken actual = null; + JToken expected = null; + + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void When_both_tokens_represent_the_same_json_content_they_should_be_treated_as_equivalent() + { + // Arrange + string json = + """ + { + friends: + [{ + id: 123, + name: "John Doe" + }, { + id: 456, + name: "Jane Doe", + kids: + [ + "Jimmy", + "James" + ] + } + ] + } + """; + + var a = JToken.Parse(json); + var b = JToken.Parse(json); + + // Act & Assert + a.Should().BeEquivalentTo(a); + b.Should().BeEquivalentTo(b); + a.Should().BeEquivalentTo(b); + } + + public static TheoryData FailingBeEquivalentCases => new() + { + { + null, + "{ id: 2 }", + "is null" + }, + { + "{ id: 1 }", + null, + "is not null" + }, + { + "{ items: [] }", + "{ items: 2 }", + "has an array instead of an integer at $.items" + }, + { + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "{ items: [ \"fork\", \"knife\" ] }", + "has 3 elements instead of 2 at $.items" + }, + { + "{ items: [ \"fork\", \"knife\" ] }", + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "has 2 elements instead of 3 at $.items" + }, + { + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "{ items: [ \"fork\", \"spoon\", \"knife\" ] }", + "has a different value at $.items[1]" + }, + { + "{ tree: { } }", + "{ tree: \"oak\" }", + "has an object instead of a string at $.tree" + }, + { + "{ tree: { leaves: 10} }", + "{ tree: { branches: 5, leaves: 10 } }", + "misses property $.tree.branches" + }, + { + "{ tree: { branches: 5, leaves: 10 } }", + "{ tree: { leaves: 10} }", + "has extra property $.tree.branches" + }, + { + "{ tree: { leaves: 5 } }", + "{ tree: { leaves: 10} }", + "has a different value at $.tree.leaves" + }, + { + "{ eyes: \"blue\" }", + "{ eyes: [] }", + "has a string instead of an array at $.eyes" + }, + { + "{ eyes: \"blue\" }", + "{ eyes: 2 }", + "has a string instead of an integer at $.eyes" + }, + { + "{ id: 1 }", + "{ id: 2 }", + "has a different value at $.id" + } + }; + + [Theory] + [MemberData(nameof(FailingBeEquivalentCases))] + public void When_both_tokens_are_not_equivalent_it_should_throw_and_mention_the_difference( + string actualJson, string expectedJson, string expectedDifference) + { + // Arrange + var actual = (actualJson != null) ? JToken.Parse(actualJson) : null; + var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null; + + var expectedMessage = + $"JSON document {expectedDifference}." + + "Actual document" + + $"{Format(actual, true)}" + + "was expected to be equivalent to" + + $"{Format(expected, true)}."; + + // Act & Assert + actual.Should().Invoking(x => x.BeEquivalentTo(expected)) + .Should().Throw() + .WithMessage(expectedMessage); + } + + [Theory] + [MemberData(nameof(PropertiesDifferingBetweenTwoTokens))] + public void When_properties_differ_between_two_tokens_it_should_not_treat_them_as_equivalent(JToken actual, JToken expected, string expectedDifference) + { + // Act & Assert + var expectedMessage = + $"JSON document {expectedDifference}." + + "Actual document" + + $"{Format(actual, true)}" + + "was expected to be equivalent to" + + $"{Format(expected, true)}."; + + actual.Should().Invoking(x => x.BeEquivalentTo(expected)) + .Should().Throw() + .WithMessage(expectedMessage); + } + + public static TheoryData PropertiesDifferingBetweenTwoTokens => new() + { + { + new JProperty("eyes", "blue"), + new JArray(), + "has a property instead of an array at $" + }, + { + new JProperty("eyes", "blue"), + new JProperty("hair", "black"), + "has a different name at $" + }, + }; + + [Fact] + public void When_both_property_values_are_null_it_should_treat_them_as_equivalent() + { + // Arrange + var actual = JToken.Parse("{ \"id\": null }"); + var expected = JToken.Parse("{ \"id\": null }"); + + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } + + [Theory] + [MemberData(nameof(JsonArraysHavingTheSamePropertiesInTheSameOrder))] + public void When_two_json_arrays_have_the_same_properties_in_the_same_order_they_should_be_treated_as_equivalent(JArray actual, JArray expected) + { + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } + + public static TheoryData JsonArraysHavingTheSamePropertiesInTheSameOrder => new() + { + { + new JArray(1, 2, 3), + new JArray(1, 2, 3) + }, + { + new JArray("blue", "green"), + new JArray("blue", "green") + }, + { + new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")), + new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")) + }, + }; + + [Theory] + [MemberData(nameof(JsonArraysHavingTheSamePropertiesInDifferentOrder))] + public void When_only_the_order_of_properties_differ_they_should_be_treated_as_equivalent(JToken actual, JToken expected) + { + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } + + public static TheoryData JsonArraysHavingTheSamePropertiesInDifferentOrder => new() + { + { + JToken.Parse("{ friends: [{ id: 123, name: \"Corby Page\" }, { id: 456, name: \"Carter Page\" }] }"), + JToken.Parse("{ friends: [{ name: \"Corby Page\", id: 123 }, { id: 456, name: \"Carter Page\" }] }") + }, + { + JToken.Parse("{ id: 2, admin: true }"), + JToken.Parse("{ admin: true, id: 2}") + }, + }; + + [Fact] + public void When_a_token_is_compared_to_its_string_representation_they_should_be_treated_as_equivalent() + { + // Arrange + string jsonString = + """ + { + friends: + [{ + id: 123, + name: "John Doe" + }, { + id: 456, + name: "Jane Doe", + kids: + [ + "Jimmy", + "James" + ] + } + ] + } + """; + + var actualJSON = JToken.Parse(jsonString); + + // Act & Assert + actualJSON.Should().BeEquivalentTo(jsonString); + } + + [Fact] + public void When_checking_non_equivalency_with_an_invalid_expected_string_it_should_provide_a_clear_error_message() + { + // Arrange + var actualJson = JToken.Parse("{ \"id\": null }"); + var expectedString = "{ invalid JSON }"; + + // Act & Assert + actualJson.Should().Invoking(x => x.BeEquivalentTo(expectedString)) + .Should().Throw() + .WithMessage($"Unable to parse expected JSON string:{expectedString}*") + .WithInnerException(); + } + + [Fact] + public void When_checking_for_non_equivalency_with_an_unparseable_string_it_should_provide_a_clear_error_message() + { + // Arrange + var actualJson = JToken.Parse("{ \"id\": null }"); + var unexpectedString = "{ invalid JSON }"; + + // Act & Assert + actualJson.Should().Invoking(x => x.NotBeEquivalentTo(unexpectedString)) + .Should().Throw() + .WithMessage($"Unable to parse unexpected JSON string:{unexpectedString}*") + .WithInnerException(); + } + + [Fact] + public void When_specifying_a_reason_why_a_token_should_be_equivalent_it_should_use_that_in_the_error_message() + { + // Arrange + var subject = JToken.Parse("{ child: { subject: 'foo' } }"); + var expected = JToken.Parse("{ child: { expected: 'bar' } }"); + + var expectedMessage = + "JSON document misses property $.child.expected." + + "Actual document" + + $"{Format(subject, true)}" + + "was expected to be equivalent to" + + $"{Format(expected, true)} " + + "because we want to test the failure message."; + + // Act & Assert + subject.Should().Invoking(x => x.BeEquivalentTo(expected, "we want to test the failure {0}", "message")) + .Should().Throw() + .WithMessage(expectedMessage); + } + + [Fact] + public void When_property_values_differ_a_non_equivalency_check_should_succeed() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1 }"); + var expected = JToken.Parse("{ \"id\": 2 }"); + + // Act & Assert + actual.Should().NotBeEquivalentTo(expected); + } + + [Fact] + public void When_two_tokens_are_the_same_the_non_equivalency_check_should_throw() + { + // Arrange + var a = JToken.Parse("{ \"id\": 1 }"); + var b = JToken.Parse("{ \"id\": 1 }"); + + // Act & Assert + a.Invoking(x => x.Should().NotBeEquivalentTo(b)) + .Should().Throw() + .WithMessage($"Expected JSON document not to be equivalent to {Format(b)}."); + } + + [Fact] + public void When_a_token_is_equal_to_its_string_representation_the_non_equivalency_check_should_throw() + { + // Arrange + string jsonString = "{ \"id\": 1 }"; + var actualJson = JToken.Parse(jsonString); + + // Act + Action action = () => actualJson.Should().NotBeEquivalentTo(jsonString); + + // Assert + action.Should().Throw() + .WithMessage("Expected JSON document not to be equivalent*"); + } + + [Fact] + public void When_a_float_is_within_approximation_check_should_succeed() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should().BeEquivalentTo(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-3)) + .WhenTypeIs()); + } + + [Fact] + public void When_a_float_is_not_within_approximation_check_should_throw() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should(). + Invoking(x => x.BeEquivalentTo(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-5)) + .WhenTypeIs())) + .Should().Throw() + .WithMessage("JSON document has a different value at $.id.*"); + } + + [Fact] + public void When_the_value_of_a_property_contains_curly_braces_the_equivalency_check_should_not_choke_on_them() + { + // Arrange + var actual = JToken.Parse(@"{ ""{a1}"": {b: 1 }}"); + var expected = JToken.Parse(@"{ ""{a1}"": {b: 2 }}"); + + // Act & Assert + var expectedMessage = + "JSON document has a different value at $.{a1}.b." + + "Actual document" + + $"{Format(actual, true)}" + + "was expected to be equivalent to" + + $"{Format(expected, true)}."; + + actual.Should().Invoking(x => x.BeEquivalentTo(expected)) + .Should().Throw() + .WithMessage(expectedMessage); + } + + #endregion (Not)BeEquivalentTo + + #region (Not)HaveValue + + [Fact] + public void When_the_token_has_the_expected_value_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject["id"].Should().HaveValue("42"); + } + + [Fact] + public void When_the_token_is_null_then_asserting_on_a_value_expectation_should_throw() + { + // Arrange + JToken subject = null; + + // Act + Action act = () => subject.Should().HaveValue("foo"); + + // Assert + act.Should().Throw().WithMessage("Expected*foo*was*null*"); + } + + [Fact] + public void When_the_token_has_another_value_than_expected_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject["id"].Should().Invoking(x => x.HaveValue("43", "because foo")) + .Should().Throw() + .WithMessage("Expected JSON property \"id\" to have value \"43\" because foo, but found \"42\"."); + } + + [Fact] + public void When_the_token_does_not_have_the_unexpected_value_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 43 }"); + + // Act & Assert + subject["id"].Should().NotHaveValue("42"); + } + + [Fact] + public void When_the_token_is_null_assertions_on_not_having_a_value_should_throw() + { + // Arrange + JToken subject = null; + + // Act + Action act = () => subject.Should().NotHaveValue("foo"); + + // Assert + act.Should().Throw().WithMessage("Did not expect*foo*was*null*"); + } + + [Fact] + public void When_the_token_has_a_value_that_it_was_not_supposed_to_have_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject["id"].Should().Invoking(x => x.NotHaveValue("42", "because foo")) + .Should().Throw() + .WithMessage("Did not expect JSON property \"id\" to have value \"42\" because foo."); + } + + #endregion (Not)HaveValue + + #region (Not)MatchRegex + + [Fact] + public void When_a_tokens_value_matches_the_regex_pattern_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject["id"].Should().MatchRegex("\\d{2}"); + } + + [Fact] + public void When_a_tokens_value_does_not_match_the_regex_pattern_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 'not two digits' }"); + + // Act & Assert + subject["id"].Should().Invoking(x => x.MatchRegex("\\d{2}", "because foo")) + .Should().Throw() + .WithMessage("Expected JSON property \"id\" to match regex pattern \"\\d{2}\" because foo, but found \"not two digits\"."); + } + + [Fact] + public void When_a_tokens_value_does_not_match_the_regex_pattern_and_that_is_expected_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 'not two digits' }"); + + // Act & Assert + subject["id"].Should().NotMatchRegex("\\d{2}"); + } + + [Fact] + public void When_a_tokens_value_matches_the_regex_pattern_unexpectedly_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject["id"].Should().Invoking(x => x.NotMatchRegex("\\d{2}", "because foo")) + .Should().Throw() + .WithMessage("Did not expect JSON property \"id\" to match regex pattern \"\\d{2}\" because foo."); + } + + #endregion (Not)MatchRegex + + #region (Not)HaveElement + + [Fact] + public void When_the_token_has_a_property_with_the_specified_key_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject.Should().HaveElement("id"); + } + + [Fact] + public void When_the_token_does_not_have_the_specified_property_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject.Should().Invoking(x => x.HaveElement("name", "because foo")) + .Should().Throw() + .WithMessage($"Expected JSON document {Format(subject)} to have element \"name\" because foo, but no such element was found."); + } + + [Fact] + public void When_the_token_does_not_have_the_specified_element_and_that_was_expected_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject.Should().NotHaveElement("name"); + } + + [Fact] + public void When_the_token_has_an_unexpected_element_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); + + // Act & Assert + subject.Should().Invoking(x => x.NotHaveElement("id", "because foo")) + .Should().Throw() + .WithMessage($"Did not expect JSON document {Format(subject)} to have element \"id\" because foo."); + } + + #endregion (Not)HaveElement + + #region ContainSingleItem + + [Fact] + public void When_the_token_has_a_single_child_and_that_was_expected_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ id: 42 }"); + + // Act + Action act = () => subject.Should().ContainSingleItem(); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_the_token_has_a_single_child_it_should_return_that_child_for_chaining() + { + // Arrange + var subject = JToken.Parse("{ id: 42 }"); + + // Act + var element = subject.Should().ContainSingleItem().Which; + + // Assert + element.Should().BeEquivalentTo(new JProperty("id", 42)); + } + + [Fact] + public void When_the_token_is_null_then_asserting_a_single_child_should_throw_with_a_clear_failure() + { + // Arrange + JToken subject = null; + + // Act + Action act = () => subject.Should().ContainSingleItem("null is not allowed"); + + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document to contain a single item because null is not allowed, but found ."); + } + + [Fact] + public void When_the_token_is_an_empty_object_then_the_assertion_on_a_single_item_should_throw() + { + // Arrange + var subject = JToken.Parse("{ }"); + + // Act + Action act = () => subject.Should().ContainSingleItem("less is not allowed"); + + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document * to contain a single item because less is not allowed, but the collection is empty."); + } + + [Fact] + public void When_the_token_contains_multiple_properties_then_the_single_item_assertion_should_throw() + { + // Arrange + var subject = JToken.Parse("{ id: 42, admin: true }"); + + // Act + Action act = () => subject.Should().ContainSingleItem("more is not allowed"); + + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document*id*42*admin*true*to contain a single item because more is not allowed, but found*"); + } + + [Fact] + public void When_the_token_is_an_array_with_a_single_property_then_that_should_satisfy_the_single_item_assertion() + { + // Arrange + var subject = JToken.Parse("[{ id: 42 }]"); + + // Act + Action act = () => subject.Should().ContainSingleItem(); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_the_token_is_an_array_with_a_single_property_the_single_item_assertion_should_return_that_item_for_chaining() + { + // Arrange + var subject = JToken.Parse("[{ id: 42 }]"); + + // Act + var element = subject.Should().ContainSingleItem().Which; + + // Assert + element.Should().BeEquivalentTo(JToken.Parse("{ id: 42 }")); + } + + [Fact] + public void When_the_token_is_an_empty_array_then_an_assertion_for_a_single_item_should_throw() + { + // Arrange + var subject = JToken.Parse("[]"); + + // Act + Action act = () => subject.Should().ContainSingleItem("less is not allowed"); + + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document [] to contain a single item because less is not allowed, but the collection is empty."); + } + + [Fact] + public void When_the_token_is_an_array_with_multiple_items_asserting_for_a_single_item_should_throw() + { + // Arrange + var subject = JToken.Parse("[1, 2]"); + + // Act + Action act = () => subject.Should().ContainSingleItem("more is not allowed"); + + // Assert + string formattedSubject = Format(subject); + + act.Should().Throw() + .WithMessage($"Expected JSON document {formattedSubject} to contain a single item because more is not allowed, but found {formattedSubject}."); + } + + #endregion ContainSingleItem + + #region HaveCount + + [Fact] + public void When_the_number_of_items_match_the_expectation_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ id: 42, admin: true }"); + + // Act + Action act = () => subject.Should().HaveCount(2); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_the_number_of_items_match_the_expectation_it_should_allow_chaining_more_assertions() + { + // Arrange + var subject = JToken.Parse("{ id: 42 }"); + + // Act + JTokenAssertions and = subject.Should().HaveCount(1).And; + + // Assert + and.BeEquivalentTo(subject); + } + + [Fact] + public void When_the_token_is_null_then_an_assertion_on_the_count_should_throw() + { + // Arrange + JToken subject = null; + + // Act + Action act = () => subject.Should().HaveCount(1, "null is not allowed"); + + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document to contain 1 item(s) because null is not allowed, but found ."); + } + + [Fact] + public void When_expecting_a_different_number_of_elements_than_the_actual_number_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ }"); + + // Act + Action act = () => subject.Should().HaveCount(1, "numbers matter"); + + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document * to contain 1 item(s) because numbers matter, but found 0*"); + } + + [Fact] + public void When_expecting_the_actual_number_of_array_items_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("[ 'Hello', 'World!' ]"); + + // Act + Action act = () => subject.Should().HaveCount(2); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_expecting_a_different_number_of_array_items_than_the_actual_number_it_should_fail() + { + // Arrange + var subject = JToken.Parse("[ 'Hello', 'World!' ]"); + + // Act + Action act = () => subject.Should().HaveCount(3, "the more the better"); + + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document * to contain 3 item(s) because the more the better, but found 2*"); + } + + #endregion HaveCount + + #region ContainSubtree + + [Fact] + public void When_all_expected_subtree_properties_match_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', baz: 'baz'} "); + + // Act + Action act = () => subject.Should().ContainSubtree(" { foo: 'foo', baz: 'baz' } "); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_deep_subtree_matches_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } }} "); + + // Act + Action act = () => subject.Should().ContainSubtree(" { child: { grandchild: { tag: 'abrakadabra' } } } "); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_array_elements_are_matching_within_a_nested_structure_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', items: [ { id: 1 }, { id: 2 }, { id: 3 } ] } "); + + // Act + Action act = () => subject.Should().ContainSubtree(" { items: [ { id: 1 }, { id: 3 } ] } "); + + // Assert + act.Should().NotThrow(); + } + + public static TheoryData FailingContainSubtreeCases => new() + { + { + null, + "{ id: 2 }", + "is null" + }, + { + "{ id: 1 }", + null, + "is not null" + }, + { + "{ foo: 'foo', bar: 'bar' }", + "{ baz: 'baz' }", + "misses property $.baz" + }, + { + "{ items: [] }", + "{ items: 2 }", + "has an array instead of an integer at $.items" + }, + { + "{ items: [ \"fork\", \"knife\" ] }", + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "misses expected element $.items[2]" + }, + { + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "{ items: [ \"fork\", \"spoon\", \"knife\" ] }", + "has expected element $.items[2] in the wrong order" + }, + { + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "{ items: [ \"fork\", \"fork\" ] }", + "has a different value at $.items[1]" + }, + { + "{ tree: { } }", + "{ tree: \"oak\" }", + "has an object instead of a string at $.tree" + }, + { + "{ tree: { leaves: 10} }", + "{ tree: { branches: 5, leaves: 10 } }", + "misses property $.tree.branches" + }, + { + "{ tree: { leaves: 5 } }", + "{ tree: { leaves: 10} }", + "has a different value at $.tree.leaves" + }, + { + "{ eyes: \"blue\" }", + "{ eyes: [] }", + "has a string instead of an array at $.eyes" + }, + { + "{ eyes: \"blue\" }", + "{ eyes: 2 }", + "has a string instead of an integer at $.eyes" + }, + { + "{ id: 1 }", + "{ id: 2 }", + "has a different value at $.id" + }, + { + "{ items: [ { id: 1 }, { id: 3 }, { id: 5 } ] }", + "{ items: [ { id: 1 }, { id: 2 } ] }", + "has a different value at $.items[1].id" + }, + { + "{ foo: '1' }", + "{ foo: 1 }", + "has a string instead of an integer at $.foo" + }, + { + "{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } } }", + "{ child: { grandchild: { tag: 'ooops' } } }", + "has a different value at $.child.grandchild.tag" + } + }; + + [Theory] + [MemberData(nameof(FailingContainSubtreeCases))] + public void When_some_JSON_does_not_contain_all_elements_from_a_subtree_it_should_throw( + string actualJson, string expectedJson, string expectedDifference) + { + // Arrange + var actual = (actualJson != null) ? JToken.Parse(actualJson) : null; + var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null; + + // Act + Action action = () => actual.Should().ContainSubtree(expected); + + // Assert + action.Should().Throw() + .WithMessage( + $"JSON document {expectedDifference}.{Environment.NewLine}" + + $"Actual document{Environment.NewLine}" + + $"{Format(actual, true)}{Environment.NewLine}" + + $"was expected to contain{Environment.NewLine}" + + $"{Format(expected, true)}.{Environment.NewLine}"); + } + + [Fact] + public void When_checking_subtree_with_an_invalid_expected_string_it_should_provide_a_clear_error_message() + { + // Arrange + var actualJson = JToken.Parse("{ \"id\": null }"); + var invalidSubtree = "{ invalid JSON }"; + + // Act & Assert + actualJson.Should().Invoking(x => x.ContainSubtree(invalidSubtree)) + .Should().Throw() + .WithMessage($"Unable to parse subtree JSON string:{invalidSubtree}*") + .WithInnerException(); + } + + [Fact] + public void Assert_property_with_approximation_succeeds() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should().ContainSubtree(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-3)) + .WhenTypeIs()); + } + + [Fact] + public void Can_assert_on_a_field_with_approximation() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should(). + Invoking(x => x.ContainSubtree(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-5)) + .WhenTypeIs())) + .Should().Throw() + .WithMessage("JSON document has a different value at $.id.*"); + } + + #endregion + + private static string Format(JToken value, bool useLineBreaks = false) + { + var output = new FormattedObjectGraph(100); + + new JTokenFormatter().Format(value, output, new FormattingContext + { + UseLineBreaks = useLineBreaks + }, null); + + return output.ToString(); + } +} diff --git a/Tests/FluentAssertions.Json.Specs/JTokenComparerSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenComparerSpecs.cs new file mode 100644 index 00000000..45e9b34a --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/JTokenComparerSpecs.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace FluentAssertions.Json.Specs; + +public class JTokenComparerSpecs +{ + private static readonly IComparer Comparer = + Type.GetType("FluentAssertions.Json.Common.JTokenExtensions, FluentAssertions.Json")! + .GetField("Comparer", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)! + .GetValue(null) as IComparer; + + [Fact] + public void Should_return_zero_for_same_reference() + { + // Arrange + var token = JToken.Parse(@"{""a"":1}"); + + // Act & Assert + Comparer.Compare(token, token).Should().Be(0); + } + + [Fact] + public void Should_handle_nulls() + { + // Arrange + var token = JToken.Parse("1"); + + // Act & Assert + Comparer.Compare(null, token).Should().Be(-1); + Comparer.Compare(token, null).Should().Be(1); + Comparer.Compare(null, null).Should().Be(0); + } + + [Fact] + public void Should_compare_different_types() + { + // Arrange + var obj = JToken.Parse(@"{""a"":1}"); + var arr = JToken.Parse("[1]"); + + // Act & Assert + Comparer.Compare(obj, arr).Should().NotBe(0); + } + + [Fact] + public void Should_compare_jvalues() + { + // Arrange + var v1 = new JValue(1); + var v2 = new JValue(2); + + // Act & Assert + Comparer.Compare(v1, v2).Should().Be(-1); + Comparer.Compare(v2, v1).Should().Be(1); + Comparer.Compare(v1, new JValue(1)).Should().Be(0); + } + + [Fact] + public void Should_compare_jarrays_by_count_and_elements() + { + // Arrange + var arr1 = JArray.Parse("[1,2]"); + var arr2 = JArray.Parse("[1,2,3]"); + var arr3 = JArray.Parse("[1,3]"); + var arr4 = JArray.Parse("[1,2,3]"); + + // Act & Assert + Comparer.Compare(arr1, arr2).Should().Be(-1); + Comparer.Compare(arr1, arr3).Should().Be(-1); + Comparer.Compare(arr3, arr1).Should().Be(1); + Comparer.Compare(arr2, arr4).Should().Be(0); + } + + [Fact] + public void Should_compare_jobjects_by_count_and_properties() + { + // Arrange + var obj1 = JObject.Parse(@"{""a"":1}"); + var obj2 = JObject.Parse(@"{""a"":1,""b"":2}"); + var obj3 = JObject.Parse(@"{""a"":2}"); + var obj4 = JObject.Parse(@"{""a"":1,""b"":2}"); + var obj5 = JObject.Parse(@"{""b"":2}"); + + // Act & Assert + Comparer.Compare(obj1, obj2).Should().Be(-1); + Comparer.Compare(obj1, obj3).Should().Be(-1); + Comparer.Compare(obj3, obj1).Should().Be(1); + Comparer.Compare(obj2, obj4).Should().Be(0); + Comparer.Compare(obj1, obj5).Should().Be(-1); + Comparer.Compare(obj5, obj1).Should().Be(1); + } + + [Fact] + public void Should_compare_jproperties_by_name_and_value() + { + // Arrange + var prop1 = new JProperty("a", 1); + var prop2 = new JProperty("b", 1); + var prop3 = new JProperty("a", 2); + var prop4 = new JProperty("a", 1); + + // Act & Assert + Comparer.Compare(prop1, prop2).Should().Be(-1); + Comparer.Compare(prop1, prop3).Should().Be(-1); + Comparer.Compare(prop3, prop1).Should().Be(1); + Comparer.Compare(prop4, prop1).Should().Be(0); + Comparer.Compare(prop2, prop3).Should().Be(1); + Comparer.Compare(prop3, prop2).Should().Be(-1); + } + + [Fact] + public void Should_compare_jconstructors_by_name() + { + // Arrange + var ctor1 = new JConstructor("foo", new JValue(1)); + var ctor2 = new JConstructor("bar", new JValue(1)); + + // Act & Assert + Comparer.Compare(ctor1, ctor2).Should().BeGreaterThan(0); // "foo" > "bar" + } + + [Fact] + public void Should_compare_jconstructors_by_argument_count() + { + // Arrange + var ctor1 = new JConstructor("foo", new JValue(1)); + var ctor2 = new JConstructor("foo", new JValue(1), new JValue(2)); + + // Act & Assert + Comparer.Compare(ctor1, ctor2).Should().Be(-1); + } + + [Fact] + public void Should_compare_jconstructors_by_argument_values() + { + // Arrange + var ctor1 = new JConstructor("foo", new JValue(1), new JValue(2)); + var ctor2 = new JConstructor("foo", new JValue(1), new JValue(3)); + + // Act & Assert + Comparer.Compare(ctor1, ctor2).Should().Be(-1); + } + + [Fact] + public void Should_return_zero_for_equal_jconstructors() + { + // Arrange + var ctor1 = new JConstructor("foo", new JValue(1), new JValue(2)); + var ctor2 = new JConstructor("foo", new JValue(1), new JValue(2)); + + // Act & Assert + Comparer.Compare(ctor1, ctor2).Should().Be(0); + } +} diff --git a/Tests/FluentAssertions.Json.Specs/JTokenFormatterSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenFormatterSpecs.cs new file mode 100644 index 00000000..69ce6bbf --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/JTokenFormatterSpecs.cs @@ -0,0 +1,57 @@ +using FluentAssertions.Formatting; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace FluentAssertions.Json.Specs; + +// ReSharper disable InconsistentNaming +public class JTokenFormatterSpecs +{ + public JTokenFormatterSpecs() + { + Formatter.AddFormatter(new JTokenFormatter()); + } + + [Fact] + public void Should_Handle_JToken() + { + // Act / Arrange + var actual = Formatter.ToString(JToken.Parse("{}")); + + // Assert + actual.Should().Be("{}"); + } + + [Fact] + public void Should_preserve_indenting() + { + // Arrange + var json = JToken.Parse("{ \"id\":1 }"); + + // Act + var actual = Formatter.ToString(json, new FormattingOptions + { + UseLineBreaks = true + }); + + // Assert + actual.Should().Be(json.ToString(Newtonsoft.Json.Formatting.Indented)); + } + + [Fact] + public void Should_Remove_line_breaks_and_indenting() + { + // Arrange + var json = JToken.Parse("{ \"id\":1 }"); + + // Act + // ReSharper disable once RedundantArgumentDefaultValue + var actual = Formatter.ToString(json, new FormattingOptions + { + UseLineBreaks = false + }); + + // Assert + actual.Should().Be(json.ToString().RemoveNewLines()); + } +} diff --git a/Tests/FluentAssertions.Json.Specs/JsonAssertionExtensionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JsonAssertionExtensionsSpecs.cs new file mode 100644 index 00000000..f5039199 --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/JsonAssertionExtensionsSpecs.cs @@ -0,0 +1,33 @@ +using FluentAssertions; +using FluentAssertions.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +// NOTE that we are using both namespaces 'FluentAssertions' & 'FluentAssertions.Json' from an external namespace to force compiler disambiguation warnings +namespace SomeOtherNamespace; + +// ReSharper disable InconsistentNaming +public class JsonAssertionExtensionsSpecs +{ + [Fact] + public void Should_Provide_Unambiguos_JTokenAssertions() + { + // Arrange + var assertions = new[] + { + JToken.Parse("{\"token\":\"value\"}").Should() + , new JProperty("property","value").Should() + , new JObject(new JProperty("object", "value")).Should() + , new JArray(new[] { 42, 43 }).Should() + , new JConstructor("property","value").Should() + , new JValue("value").Should() + , new JRaw("value").Should() + }; + + // Act & Assert + foreach (var sut in assertions) + { + ((object)sut).Should().BeOfType("extensions should provide assertions for all JSon primitives, i.e. JObject, JToken and JProperty"); + } + } +} diff --git a/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs new file mode 100644 index 00000000..2a5987de --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs @@ -0,0 +1,42 @@ +using System; +using FluentAssertions.Equivalency; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace FluentAssertions.Json.Specs; + +[Collection("AssertionOptionsSpecs")] +public class JsonAssertionOptionsSpecs +{ + [Fact] + public void Local_equivalency_options_are_applied_on_top_of_global_equivalency_options() + { + using var assertionOptions = new TempDefaultAssertionOptions(e => e + .Using(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.1)) + .WhenTypeIs()); + + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should().BeEquivalentTo(expected, options => options); + } + + private sealed class TempDefaultAssertionOptions : IDisposable + { + public TempDefaultAssertionOptions(Func config) + { + AssertionConfiguration.Current.Equivalency.Modify(config); + } + + public void Dispose() + { + AssertionConfiguration.Current.Equivalency.Modify(_ => new()); + } + } +} + +// Due to tests that call AssertionOptions +[CollectionDefinition("AssertionOptionsSpecs", DisableParallelization = true)] +public class AssertionOptionsSpecsDefinition; diff --git a/Tests/FluentAssertions.Json.Specs/Models/AddressDto.cs b/Tests/FluentAssertions.Json.Specs/Models/AddressDto.cs new file mode 100644 index 00000000..799186dc --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/Models/AddressDto.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json; + +namespace FluentAssertions.Json.Specs.Models; + +// ReSharper disable UnusedMember.Global +public class AddressDto +{ + public string AddressLine1 { get; set; } + + public string AddressLine2 { get; set; } + + public string AddressLine3 { get; set; } +} + +public class DerivedFromAddressDto : AddressDto +{ + [JsonIgnore] + public DateTime LastUpdated { get; set; } +} + +// ReSharper restore UnusedMember.Global diff --git a/Tests/FluentAssertions.Json.Specs/Models/EmploymentDto.cs b/Tests/FluentAssertions.Json.Specs/Models/EmploymentDto.cs new file mode 100644 index 00000000..cd4f013a --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/Models/EmploymentDto.cs @@ -0,0 +1,11 @@ +namespace FluentAssertions.Json.Specs.Models; + +// ReSharper disable UnusedMember.Global +public class EmploymentDto +{ + public string JobTitle { get; set; } + + public string PhoneNumber { get; set; } +} + +// ReSharper restore UnusedMember.Global diff --git a/Tests/FluentAssertions.Json.Specs/Models/PocoWithIgnoredProperty.cs b/Tests/FluentAssertions.Json.Specs/Models/PocoWithIgnoredProperty.cs new file mode 100644 index 00000000..b368f50f --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/Models/PocoWithIgnoredProperty.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace FluentAssertions.Json.Specs.Models; + +public class PocoWithIgnoredProperty +{ + public int Id { get; set; } + + [JsonIgnore] + public string Name { get; set; } +} diff --git a/Tests/FluentAssertions.Json.Specs/Models/PocoWithNoDefaultConstructor.cs b/Tests/FluentAssertions.Json.Specs/Models/PocoWithNoDefaultConstructor.cs new file mode 100644 index 00000000..0910a461 --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/Models/PocoWithNoDefaultConstructor.cs @@ -0,0 +1,15 @@ +namespace FluentAssertions.Json.Specs.Models; + +public class PocoWithNoDefaultConstructor +{ + public int Id { get; } + + /// + /// Newtonsoft.Json will deserialise this successfully if the parameter name id the same as the property + /// + /// DO NOT CHANGE THE NAME OF THIS PARAMETER + public PocoWithNoDefaultConstructor(int value) + { + Id = value; + } +} diff --git a/Tests/FluentAssertions.Json.Specs/Models/PocoWithStructure.cs b/Tests/FluentAssertions.Json.Specs/Models/PocoWithStructure.cs new file mode 100644 index 00000000..d9bd24c8 --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/Models/PocoWithStructure.cs @@ -0,0 +1,13 @@ +namespace FluentAssertions.Json.Specs.Models; + +// ReSharper disable UnusedMember.Global +public class PocoWithStructure +{ + public int Id { get; set; } + + public AddressDto Address { get; set; } + + public EmploymentDto Employment { get; set; } +} + +// ReSharper restore UnusedMember.Global diff --git a/Tests/FluentAssertions.Json.Specs/Models/SimplePocoWithPrimitiveTypes.cs b/Tests/FluentAssertions.Json.Specs/Models/SimplePocoWithPrimitiveTypes.cs new file mode 100644 index 00000000..c43fd39d --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/Models/SimplePocoWithPrimitiveTypes.cs @@ -0,0 +1,31 @@ +using System; + +namespace FluentAssertions.Json.Specs.Models; + +// ReSharper disable UnusedMember.Global +public class SimplePocoWithPrimitiveTypes +{ + public int Id { get; set; } + + public Guid GlobalId { get; set; } + + public string Name { get; set; } + + public DateTime DateOfBirth { get; set; } + + public decimal Height { get; set; } + + public double Weight { get; set; } + + public float ShoeSize { get; set; } + + public bool IsActive { get; set; } + +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Image { get; set; } +#pragma warning restore CA1819 // Properties should not return arrays + + public char Category { get; set; } +} + +// ReSharper restore UnusedMember.Global diff --git a/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs b/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs new file mode 100644 index 00000000..35279b58 --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs @@ -0,0 +1,174 @@ +using System; +using FluentAssertions; +using FluentAssertions.Json; +using FluentAssertions.Json.Specs.Models; +using Xunit; + +// NOTE that we are using both namespaces 'FluentAssertions' & 'FluentAssertions.Json' from an external namespace to force compiler disambiguation warnings +// ReSharper disable CheckNamespace +namespace SomeOtherNamespace; + +// ReSharper restore CheckNamespace +public class ShouldBeJsonSerializableTests +{ + [Fact] + public void Simple_poco_should_be_serializable() + { + // arrange + var target = new SimplePocoWithPrimitiveTypes + { + Id = 1, + GlobalId = Guid.NewGuid(), + Name = "Name", + DateOfBirth = DateTime.UtcNow, + Height = 1, + Weight = 1, + ShoeSize = 1, + IsActive = true, + Image = [1], + Category = '1' + }; + + // act + Action act = () => target.Should().BeJsonSerializable(); + + // assert + act.Should().NotThrow(); + } + + [Fact] + public void Complex_poco_should_be_serializable() + { + // arrange + var target = new PocoWithStructure + { + Address = new AddressDto + { + AddressLine1 = "AddressLine1", + AddressLine2 = "AddressLine2", + AddressLine3 = "AddressLine3", + }, + Employment = new EmploymentDto + { + JobTitle = "JobTitle", + PhoneNumber = "PhoneNumber", + }, + Id = 1, + }; + + // act + Action act = () => target.Should().BeJsonSerializable(); + + // assert + act.Should().NotThrow(); + } + + [Fact] + public void Class_that_does_not_have_default_constructor_should_not_be_serializable() + { + // arrange + const string reasonText = "this is the reason"; + var target = new PocoWithNoDefaultConstructor(1); + + // act + Action act = () => target.Should().BeJsonSerializable(reasonText); + + // assert + act.Should().Throw() + .Which.Message.Should() + .Contain("to be JSON serializable") + .And.Contain(reasonText) + .And.Contain("but serializing") + .And.Contain("failed with"); + } + + [Fact] + public void Class_that_has_ignored_property_should_not_be_serializable_if_equivalency_options_are_not_configured() + { + // arrange + const string reasonText = "this is the reason"; + var target = new PocoWithIgnoredProperty + { + Id = 1, + Name = "Name", + }; + + // act + Action act = () => target.Should().BeJsonSerializable(reasonText); + + // assert + act.Should().Throw() + .Which.Message.Should() + .Contain("to be JSON serializable") + .And.Contain(reasonText) + .And.Contain("but serializing") + .And.Contain("failed with"); + } + + [Fact] + public void Class_that_has_ignored_property_should_be_serializable_when_equivalency_options_are_configured() + { + // arrange + var target = new PocoWithIgnoredProperty + { + Id = 1, + Name = "Name", + }; + + // act + Action act = () => target.Should().BeJsonSerializable(opts => opts.Excluding(p => p.Name)); + + // assert + act.Should().NotThrow(); + } + + [Fact] + public void Should_fail_when_instance_is_null() + { + // arrange + const SimplePocoWithPrimitiveTypes target = null; + + // act + Action act = () => target.Should().BeJsonSerializable(); + + // assert + act.Should() + .Throw(because: "This is consistent with BeBinarySerializable() and BeDataContractSerializable()") + .WithMessage("*value is null*Please provide a value for the assertion*"); + } + + [Fact] + public void Should_fail_when_subject_is_not_same_type_as_the_specified_generic_type() + { + // arrange + var target = new AddressDto(); + + // act + Action act = () => target.Should().BeJsonSerializable(); + + // assert + act.Should().Throw(because: "This is consistent with BeBinarySerializable() and BeDataContractSerializable()") + .Which.Message + .Should().Contain("is not assignable to") + .And.Contain(nameof(SimplePocoWithPrimitiveTypes)); + } + + [Fact] + public void Should_fail_when_derived_type_is_not_serializable_when_presented_as_base_class() + { + // arrange + AddressDto target = new DerivedFromAddressDto + { + AddressLine1 = "AddressLine1", + AddressLine2 = "AddressLine2", + AddressLine3 = "AddressLine3", + LastUpdated = DateTime.UtcNow, + }; + + // act + Action act = () => target.Should().BeJsonSerializable(); + + // assert + act.Should().Throw("The derived class is not serializable due to a JsonIgnore attribute"); + } +} diff --git a/Tests/FluentAssertions.Json.Specs/StringAssertionsExtensionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/StringAssertionsExtensionsSpecs.cs new file mode 100644 index 00000000..042d2c71 --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/StringAssertionsExtensionsSpecs.cs @@ -0,0 +1,72 @@ +using System; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Json.Specs; + +// ReSharper disable ExpressionIsAlwaysNull +public class StringAssertionsExtensionsSpecs +{ + #region BeValidJson + + [Fact] + public void When_checking_valid_json_BeValidJson_should_succeed() + { + // Arrange + string subject = "{ id: 42, admin: true }"; + + // Act + Action act = () => subject.Should().BeValidJson(); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_checking_valid_json_BeValidJson_should_enable_consecutive_jtoken_assertions() + { + // Arrange + string subject = "{ id: 42 }"; + + // Act + object which = subject.Should().BeValidJson().Which; + + // Assert + which.Should().BeAssignableTo(); + } + + [Fact] + public void When_checking_null_BeValidJson_should_fail() + { + // Arrange + string subject = null; + + // Act + Action act = () => subject.Should().BeValidJson("null is not allowed"); + + // Assert + act.Should().Throw() + .Which.Message.Should() + .Match("Expected subject to be valid JSON because null is not allowed, but parsing failed with \"*\"."); + } + + [Fact] + public void When_checking_invalid_json_BeValidJson_should_fail() + { + // Arrange + string subject = "invalid json"; + + // Act + Action act = () => subject.Should().BeValidJson("we like {0}", "JSON"); + + // Assert + act.Should() + .Throw() + .Which.Message.Should() + .Match("Expected subject to be valid JSON because we like JSON, but parsing failed with \"*\"."); + } + + #endregion + +} diff --git a/Tests/FluentAssertions.Json.Specs/StringExtensions.cs b/Tests/FluentAssertions.Json.Specs/StringExtensions.cs new file mode 100644 index 00000000..20729a8b --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/StringExtensions.cs @@ -0,0 +1,9 @@ +namespace FluentAssertions.Json.Specs; + +internal static class StringExtensions +{ + public static string RemoveNewLines(this string @this) + { + return @this.Replace("\n", "").Replace("\r", "").Replace("\\r\\n", ""); + } +} diff --git a/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs b/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs new file mode 100644 index 00000000..10b4cab8 --- /dev/null +++ b/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs @@ -0,0 +1,70 @@ +using System; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Json.Specs; + +public class WithoutStrictOrderingSpecs +{ + [Theory] + [MemberData(nameof(When_ignoring_ordering_BeEquivalentTo_should_succeed_sample_data))] + public void When_ignoring_ordering_BeEquivalentTo_should_succeed(string subject, string expectation) + { + // Arrange + var subjectJToken = JToken.Parse(subject); + var expectationJToken = JToken.Parse(expectation); + + // Act + subjectJToken.Should().BeEquivalentTo(expectationJToken, opt => opt.WithoutStrictOrdering()); + + // Assert + } + + public static TheoryData When_ignoring_ordering_BeEquivalentTo_should_succeed_sample_data() + { + return new TheoryData + { + { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" }, + { @"{""ids"":[1,2,3]}", @"{""ids"":[1,2,3]}" }, + { @"{""type"":2,""name"":""b""}", @"{""name"":""b"",""type"":2}" }, + { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" }, + { + @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", + @"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}" + }, + { + @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", + @"{""vals"":[{""name"":""a"",""type"":1},{""type"":2,""name"":""b""}]}" + } + }; + } + + [Theory] + [MemberData(nameof(When_not_ignoring_ordering_BeEquivalentTo_should_throw_sample_data))] + public void When_not_ignoring_ordering_BeEquivalentTo_should_throw(string subject, string expectation) + { + // Arrange + var subjectJToken = JToken.Parse(subject); + var expectationJToken = JToken.Parse(expectation); + + // Act + var action = new Func>(() => subjectJToken.Should().BeEquivalentTo(expectationJToken)); + + // Assert + action.Should().Throw(); + } + + public static TheoryData When_not_ignoring_ordering_BeEquivalentTo_should_throw_sample_data() + { + return new TheoryData + { + { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" }, + { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" }, + { + @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", + @"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}" + } + }; + } +} diff --git a/build.cake b/build.cake deleted file mode 100644 index aab03fb1..00000000 --- a/build.cake +++ /dev/null @@ -1,93 +0,0 @@ -#tool "nuget:?package=xunit.runner.console&version=2.2.0" -#tool "nuget:?package=GitVersion.CommandLine" - -////////////////////////////////////////////////////////////////////// -// ARGUMENTS -////////////////////////////////////////////////////////////////////// - -var target = Argument("target", "Default"); -var configuration = Argument("configuration", "Release"); -var toolpath = Argument("toolpath", @""); - -////////////////////////////////////////////////////////////////////// -// PREPARATION -////////////////////////////////////////////////////////////////////// - -// Define directories. -var buildDir = Directory("./Artifacts") + Directory(configuration); -GitVersion gitVersion = null; - -////////////////////////////////////////////////////////////////////// -// TASKS -////////////////////////////////////////////////////////////////////// - -Task("Clean") - .Does(() => -{ - CleanDirectory(buildDir); -}); - -Task("GitVersion").Does(() => { - gitVersion = GitVersion(new GitVersionSettings { - UpdateAssemblyInfo = true - }); -}); - -Task("Restore-NuGet-Packages") - .IsDependentOn("Clean") - .Does(() => -{ - DotNetCoreRestore(); - - NuGetRestore("./FluentAssertions.Json.sln", new NuGetRestoreSettings - { - NoCache = true, - Verbosity = NuGetVerbosity.Detailed, - ToolPath = "./build/nuget.exe" - }); -}); - -Task("Build") - .IsDependentOn("Restore-NuGet-Packages") - .Does(() => -{ - // Use MSBuild - MSBuild("./FluentAssertions.Json.sln", settings => { - settings.ToolPath = String.IsNullOrEmpty(toolpath) ? settings.ToolPath : toolpath; - settings.ToolVersion = MSBuildToolVersion.VS2017; - settings.PlatformTarget = PlatformTarget.MSIL; - settings.SetConfiguration(configuration); - }); -}); - -Task("Run-Unit-Tests") - .Does(() => -{ - XUnit2("./Tests/FluentAssertions.Json.Net45.Specs/bin/" + configuration + "/*.Specs.dll", new XUnit2Settings { }); -}); - -Task("Pack") - .IsDependentOn("GitVersion") - .Does(() => - { - NuGetPack("./src/FluentAssertions.nuspec", new NuGetPackSettings { - OutputDirectory = "./Artifacts", - Version = gitVersion.NuGetVersionV2 - }); - }); - -////////////////////////////////////////////////////////////////////// -// TASK TARGETS -////////////////////////////////////////////////////////////////////// - -Task("Default") - .IsDependentOn("GitVersion") - .IsDependentOn("Build") - .IsDependentOn("Run-Unit-Tests") - .IsDependentOn("Pack"); - -////////////////////////////////////////////////////////////////////// -// EXECUTION -////////////////////////////////////////////////////////////////////// - -RunTarget(target); diff --git a/build.cmd b/build.cmd new file mode 100755 index 00000000..b08cc590 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index fdb5b503..4634dc03 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,190 +1,74 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -<# - -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. - -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. - -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER Experimental -Tells Cake to use the latest Roslyn release. -.PARAMETER WhatIf -Performs a dry run of the build script. -No tasks will be executed. -.PARAMETER Mono -Tells Cake to use the Mono scripting engine. -.PARAMETER SkipToolPackageRestore -Skips restoring of packages. -.PARAMETER ScriptArgs -Remaining arguments are added here. - -.LINK -http://cakebuild.net - -#> - [CmdletBinding()] Param( - [string]$Script = "build.cake", - [string]$Target = "Default", - [ValidateSet("Release", "Debug")] - [string]$Configuration = "Release", - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity = "Verbose", - [switch]$Experimental, - [Alias("DryRun","Noop")] - [switch]$WhatIf, - [switch]$Mono, - [switch]$SkipToolPackageRestore, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs + [string[]]$BuildArguments ) -[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null -function MD5HashFile([string] $filePath) -{ - if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) - { - return $null - } +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" - [System.IO.Stream] $file = $null; - [System.Security.Cryptography.MD5] $md5 = $null; - try - { - $md5 = [System.Security.Cryptography.MD5]::Create() - $file = [System.IO.File]::OpenRead($filePath) - return [System.BitConverter]::ToString($md5.ComputeHash($file)) - } - finally - { - if ($file -ne $null) - { - $file.Dispose() - } - } -} +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -Write-Host "Preparing to run build script..." +########################################################################### +# CONFIGURATION +########################################################################### -if(!$PSScriptRoot){ - $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -} +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" -$TOOLS_DIR = Join-Path $PSScriptRoot "Build" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$NUGET_VERSION = "4.3.0" -$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/v$($NUGET_VERSION)/nuget.exe" -$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" -$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "STS" -# Should we use mono? -$UseMono = ""; -if($Mono.IsPresent) { - Write-Verbose -Message "Using the Mono based scripting engine." - $UseMono = "-mono" -} +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_NOLOGO = 1 -# Should we use the new Roslyn? -$UseExperimental = ""; -if($Experimental.IsPresent -and !($Mono.IsPresent)) { - Write-Verbose -Message "Using experimental version of Roslyn." - $UseExperimental = "-experimental" -} +########################################################################### +# EXECUTION +########################################################################### -# Is this a dry run? -$UseDryRun = ""; -if($WhatIf.IsPresent) { - $UseDryRun = "-dryrun" +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } } -# Make sure tools folder exists -if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path } - -# Make sure that packages.config exist. -if (!(Test-Path $PACKAGES_CONFIG)) { - Write-Verbose -Message "Downloading packages.config..." - try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { - Throw "Could not download packages.config." +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } } -} -# Try find NuGet.exe in path if not exists -#if (!(Test-Path $NUGET_EXE)) { -# Write-Verbose -Message "Trying to find nuget.exe in PATH..." -# $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } -# $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 -# if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { -# Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." -# $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName -# } -#} - -# Try download NuGet.exe if not exists -if (!(Test-Path $NUGET_EXE) -Or (Get-Item $NUGET_EXE).VersionInfo.ProductVersion -ne $NUGET_VERSION) { - Write-Verbose -Message "Downloading NuGet.exe..." - try { - (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) - } catch { - Throw "Could not download NuGet.exe." + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" } -# Save nuget.exe path to environment to be available to child processed -$ENV:NUGET_EXE = $NUGET_EXE - -# Restore tools from NuGet? -if(-Not $SkipToolPackageRestore.IsPresent) { - Push-Location - Set-Location $TOOLS_DIR - - # Check for changes in packages.config and remove installed tools if true. - [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) - if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or - ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { - Write-Verbose -Message "Missing or changed package.config hash..." - Remove-Item * -Recurse -Exclude packages.config,nuget.exe - } - - Write-Verbose -Message "Restoring tools from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occured while restoring NuGet tools." - } - else - { - $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" - } - Write-Verbose -Message ($NuGetOutput | out-string) - Pop-Location -} +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe at $CAKE_EXE" +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null } -# Start Cake -Write-Host "Running build script..." -Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" -exit $LASTEXITCODE \ No newline at end of file +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..2063070b --- /dev/null +++ b/build.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/Build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_NOLOGO=1 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/global.json b/global.json new file mode 100644 index 00000000..d07970ac --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.100", + "rollForward": "latestMajor" + } +}