diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5d2e0e..3888b95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,16 +17,16 @@ jobs: DOTNET_NOLOGO: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET SDKs - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: | - 3.1.x - 5.0.x + 6.0.x + 8.0.x - name: Run NUKE run: ./build.ps1 @@ -36,12 +36,12 @@ jobs: ApiKey: ${{ secrets.NUGETAPIKEY }} - name: coveralls - uses: coverallsapp/github-action@1.1.3 + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: TestResults/reports/lcov.info + file: TestResults/reports/lcov.info - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: path: ./Artifacts/* diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 492ae63..070dafd 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,23 +1,52 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Build Schema", - "$ref": "#/definitions/build", "definitions": { - "build": { - "type": "object", + "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": { - "ApiKey": { - "type": "string", - "description": "The key to push to Nuget" - }, - "BranchSpec": { - "type": "string", - "description": "A branch specification such as develop or refs/pull/1775/merge" - }, - "BuildNumber": { - "type": "string", - "description": "An incrementing build number as provided by the build engine" - }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" @@ -27,24 +56,8 @@ "description": "Shows the help text for this build assembly" }, "Host": { - "type": "string", "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] + "$ref": "#/definitions/Host" }, "NoLogo": { "type": "boolean", @@ -73,53 +86,39 @@ "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "ApiChecks", - "CalculateNugetVersion", - "Clean", - "CodeCoverage", - "Compile", - "Pack", - "Push", - "Restore", - "UnitTests" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "ApiChecks", - "CalculateNugetVersion", - "Clean", - "CodeCoverage", - "Compile", - "Pack", - "Push", - "Restore", - "UnitTests" - ] + "$ref": "#/definitions/ExecutableTarget" } }, "Verbosity": { - "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] + "$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" } - } -} \ No newline at end of file + ] +} diff --git a/Build/Build.cs b/Build/Build.cs index d51f8fc..8a440f2 100644 --- a/Build/Build.cs +++ b/Build/Build.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; using Nuke.Common; +using Nuke.Common.CI.GitHubActions; using Nuke.Common.Execution; using Nuke.Common.IO; using Nuke.Common.ProjectModel; @@ -11,13 +11,11 @@ using Nuke.Common.Tools.ReportGenerator; using Nuke.Common.Tools.Xunit; using Nuke.Common.Utilities.Collections; -using static Nuke.Common.IO.FileSystemTasks; -using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Tools.ReportGenerator.ReportGeneratorTasks; -[CheckBuildProjectConfigurations] [UnsetVisualStudioEnvironmentVariables] +[DotNetVerbosityMapping] class Build : NukeBuild { /* Support plugins are available for: @@ -28,19 +26,20 @@ class Build : NukeBuild */ public static int Main() => Execute(x => x.Push); - [Parameter("A branch specification such as develop or refs/pull/1775/merge")] - readonly string BranchSpec; + GitHubActions GitHubActions => GitHubActions.Instance; - [Parameter("An incrementing build number as provided by the build engine")] - readonly string BuildNumber; + 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 = "net5.0")] + [GitVersion(Framework = "net6.0")] readonly GitVersion GitVersion; AbsolutePath SourceDirectory => RootDirectory / "src"; @@ -54,9 +53,9 @@ class Build : NukeBuild Target Clean => _ => _ .Executes(() => { - SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); - TestsDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); - EnsureCleanDirectory(ArtifactsDirectory); + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(path => path.DeleteDirectory()); + TestsDirectory.GlobDirectories("**/bin", "**/obj").ForEach(path => path.DeleteDirectory()); + ArtifactsDirectory.CreateOrCleanDirectory(); }); Target CalculateNugetVersion => _ => _ @@ -75,7 +74,7 @@ class Build : NukeBuild Serilog.Log.Information("SemVer = {semver}", SemVer); }); - bool IsPullRequest => BranchSpec != null && BranchSpec.Contains("pull", StringComparison.InvariantCultureIgnoreCase); + bool IsPullRequest => GitHubActions?.IsPullRequest ?? false; Target Restore => _ => _ .DependsOn(Clean) @@ -122,7 +121,7 @@ class Build : NukeBuild DotNetTest(s => s .SetProjectFile(Solution.FluentAssertions_Json_Specs) - .SetFramework("netcoreapp3.0") + .SetFramework("net8.0") .SetConfiguration("Debug") .EnableNoBuild() .SetDataCollector("XPlat Code Coverage") @@ -134,7 +133,7 @@ class Build : NukeBuild .Executes(() => { ReportGenerator(s => s - .SetProcessToolPath(ToolPathResolver.GetPackageExecutable("ReportGenerator", "ReportGenerator.dll", framework: "net5.0")) + .SetProcessToolPath(NuGetToolPathResolver.GetPackageExecutable("ReportGenerator", "ReportGenerator.dll", framework: "net6.0")) .SetTargetDirectory(RootDirectory / "TestResults" / "reports") .AddReports(RootDirectory / "TestResults/**/coverage.cobertura.xml") .AddReportTypes("HtmlInline_AzurePipelines_Dark", "lcov") @@ -168,9 +167,9 @@ class Build : NukeBuild .OnlyWhenDynamic(() => IsTag) .Executes(() => { - IReadOnlyCollection packages = GlobFiles(ArtifactsDirectory, "*.nupkg"); + var packages = ArtifactsDirectory.GlobFiles("*.nupkg"); - Assert.NotEmpty(packages.ToList()); + Assert.NotEmpty(packages); DotNetNuGetPush(s => s .SetApiKey(ApiKey) @@ -181,5 +180,5 @@ class Build : NukeBuild (v, path) => v.SetTargetPath(path))); }); - bool IsTag => BranchSpec != null && BranchSpec.Contains("refs/tags", StringComparison.InvariantCultureIgnoreCase); + bool IsTag => BranchSpec != null && BranchSpec.Contains("refs/tags", StringComparison.OrdinalIgnoreCase); } diff --git a/Build/_build.csproj b/Build/_build.csproj index ef255b4..ba17156 100644 --- a/Build/_build.csproj +++ b/Build/_build.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net8.0 CS0649;CS0169 ..\ @@ -10,9 +10,10 @@ - - - + + + + diff --git a/Src/FluentAssertions.Json/IJsonAssertionOptions.cs b/Src/FluentAssertions.Json/IJsonAssertionOptions.cs index 4fc2bcc..ef0c2d3 100644 --- a/Src/FluentAssertions.Json/IJsonAssertionOptions.cs +++ b/Src/FluentAssertions.Json/IJsonAssertionOptions.cs @@ -15,6 +15,6 @@ public interface IJsonAssertionOptions /// /// The assertion to execute when the predicate is met. /// - IJsonAssertionRestriction Using(Action> action); + IJsonAssertionRestriction Using(Action> action); } } diff --git a/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs b/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs index da3bc9d..f499781 100644 --- a/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs +++ b/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs @@ -11,4 +11,4 @@ public interface IJsonAssertionRestriction /// public IJsonAssertionOptions WhenTypeIs() where TMemberType : TMember; } -} \ No newline at end of file +} diff --git a/Src/FluentAssertions.Json/JTokenDifferentiator.cs b/Src/FluentAssertions.Json/JTokenDifferentiator.cs index 3344e91..f5c7cf7 100644 --- a/Src/FluentAssertions.Json/JTokenDifferentiator.cs +++ b/Src/FluentAssertions.Json/JTokenDifferentiator.cs @@ -230,6 +230,7 @@ private Difference CompareValues(JValue actual, JValue expected, JPath path) return null; } + private static string Describe(JTokenType jTokenType) { return jTokenType switch diff --git a/Src/FluentAssertions.Json/JsonAssertionOptions.cs b/Src/FluentAssertions.Json/JsonAssertionOptions.cs index ccb3bd0..f51a545 100644 --- a/Src/FluentAssertions.Json/JsonAssertionOptions.cs +++ b/Src/FluentAssertions.Json/JsonAssertionOptions.cs @@ -6,7 +6,7 @@ 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 : EquivalencyAssertionOptions , IJsonAssertionOptions + public sealed class JsonAssertionOptions : EquivalencyAssertionOptions, IJsonAssertionOptions { public JsonAssertionOptions(EquivalencyAssertionOptions equivalencyAssertionOptions) : base(equivalencyAssertionOptions) { @@ -17,4 +17,4 @@ public JsonAssertionOptions(EquivalencyAssertionOptions equivalencyAssertionO return new JsonAssertionRestriction(base.Using(action)); } } -} \ No newline at end of file +} diff --git a/Src/FluentAssertions.Json/JsonAssertionRestriction.cs b/Src/FluentAssertions.Json/JsonAssertionRestriction.cs index 2de3e97..885f3f3 100644 --- a/Src/FluentAssertions.Json/JsonAssertionRestriction.cs +++ b/Src/FluentAssertions.Json/JsonAssertionRestriction.cs @@ -9,7 +9,8 @@ internal JsonAssertionRestriction(JsonAssertionOptions.Restriction this.restriction = restriction; } - public IJsonAssertionOptions WhenTypeIs() where TMemberType : TProperty + public IJsonAssertionOptions WhenTypeIs() + where TMemberType : TProperty { return (JsonAssertionOptions)restriction.WhenTypeIs(); } diff --git a/Tests/.editorconfig b/Tests/.editorconfig new file mode 100644 index 0000000..8235203 --- /dev/null +++ b/Tests/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CA1825: Avoid unnecessary zero-length array allocations. Use Array.Empty() instead +dotnet_diagnostic.CA1825.severity = none \ No newline at end of file diff --git a/Tests/Approval.Tests/ApiApproval.cs b/Tests/Approval.Tests/ApiApproval.cs index 77b8aa3..c556390 100644 --- a/Tests/Approval.Tests/ApiApproval.cs +++ b/Tests/Approval.Tests/ApiApproval.cs @@ -1,75 +1,53 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; using System.Threading.Tasks; -using DiffPlex.DiffBuilder; -using DiffPlex.DiffBuilder.Model; +using System.Xml.Linq; +using System.Xml.XPath; using PublicApiGenerator; using VerifyTests; +using VerifyTests.DiffPlex; using VerifyXunit; using Xunit; -namespace Approval.Tests -{ - [UsesVerify] - public class ApiApproval - { - [Theory] - [InlineData("net47")] - // TODO: [InlineData("netstandard2.0")] throws AssemblyResolutionException for Jonas - public Task ApproveApi(string frameworkVersion) - { - string codeBase = Assembly.GetExecutingAssembly().Location; - var uri = new UriBuilder(new Uri(codeBase)); - string assemblyPath = Uri.UnescapeDataString(uri.Path); - var containingDirectory = Path.GetDirectoryName(assemblyPath); - var configurationName = new DirectoryInfo(containingDirectory).Parent.Name; - var assemblyFile = Path.GetFullPath( - Path.Combine( - GetSourceDirectory(), - Path.Combine("..", "..", "Src", "FluentAssertions.Json", "bin", configurationName, frameworkVersion, "FluentAssertions.Json.dll"))); +namespace Approval.Tests; - var assembly = Assembly.LoadFile(Path.GetFullPath(assemblyFile)); - var publicApi = assembly.GeneratePublicApi(options: new() { }); +public class ApiApproval +{ + static ApiApproval() => VerifyDiffPlex.Initialize(OutputType.Minimal); - return Verifier - .Verify(publicApi) - .UseDirectory(Path.Combine("ApprovedApi", "FluentAssertions.Json")) - .UseStringComparer(OnlyIncludeChanges) - .UseFileName(frameworkVersion) - .DisableDiff(); - } + [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); - private static string GetSourceDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path); + return Verifier + .Verify(publicApi) + .ScrubLinesContaining("FrameworkDisplayName") + .UseDirectory(Path.Combine("ApprovedApi", "FluentAssertions.Json")) + .UseFileName(framework) + .DisableDiff(); + } - // Copied from https://github.com/VerifyTests/Verify.DiffPlex/blob/master/src/Verify.DiffPlex/VerifyDiffPlex.cs - public static Task OnlyIncludeChanges(string received, string verified, IReadOnlyDictionary _) + private class TargetFrameworksTheoryData : TheoryData + { + public TargetFrameworksTheoryData() { - var diff = InlineDiffBuilder.Diff(verified, received); - - var builder = new StringBuilder(); - foreach (var line in diff.Lines) - { - switch (line.Type) - { - case ChangeType.Inserted: - builder.Append("+ "); - break; - case ChangeType.Deleted: - builder.Append("- "); - break; - default: - // omit unchanged files - continue; - } - builder.AppendLine(line.Text); - } - - var compareResult = CompareResult.NotEqual(builder.ToString()); - return Task.FromResult(compareResult); + 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 index b926190..28df4ff 100644 --- a/Tests/Approval.Tests/Approval.Tests.csproj +++ b/Tests/Approval.Tests/Approval.Tests.csproj @@ -1,19 +1,20 @@  - net5.0 + 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 index a211f97..cd5d007 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt @@ -1,5 +1,4 @@ [assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/fluentassertions/fluentassertions.json.git")] -[assembly: System.Runtime.Versioning.TargetFramework(".NETFramework,Version=v4.7", FrameworkDisplayName=".NET Framework 4.7")] namespace FluentAssertions.Json { public interface IJsonAssertionOptions 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 0000000..cd5d007 --- /dev/null +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt @@ -0,0 +1,73 @@ +[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); + } + public interface IJsonAssertionRestriction + { + FluentAssertions.Json.IJsonAssertionOptions WhenTypeIs() + where TMemberType : TMember; + } + public class JTokenAssertions : FluentAssertions.Primitives.ReferenceTypeAssertions + { + public JTokenAssertions(Newtonsoft.Json.Linq.JToken subject) { } + 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 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.EquivalencyAssertionOptions, FluentAssertions.Json.IJsonAssertionOptions + { + public JsonAssertionOptions(FluentAssertions.Equivalency.EquivalencyAssertionOptions equivalencyAssertionOptions) { } + public FluentAssertions.Json.IJsonAssertionRestriction Using(System.Action> action) { } + } + 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.EquivalencyAssertionOptions> 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.Specs/FluentAssertions.Json.Specs.csproj b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj index e3f7094..090ed71 100644 --- a/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj +++ b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj @@ -2,32 +2,37 @@ false - net47;netcoreapp3.0 + net47;net8.0 9.0 true false - + false false false - - + - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + 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 index f0a2b3e..81cb74f 100644 --- a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs @@ -57,90 +57,74 @@ public void When_both_tokens_represent_the_same_json_content_they_should_be_trea a.Should().BeEquivalentTo(b); } - public static IEnumerable FailingBeEquivalentCases + public static TheoryData FailingBeEquivalentCases => new() { - get { - yield return new object[] - { - null, - "{ id: 2 }", - "is null" - }; - yield return new object[] - { - "{ id: 1 }", - null, - "is not null" - }; - yield return new object[] - { - "{ items: [] }", - "{ items: 2 }", - "has an array instead of an integer at $.items" - }; - yield return new object[] - { - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "{ items: [ \"fork\", \"knife\" ] }", - "has 3 elements instead of 2 at $.items" - }; - yield return new object[] - { - "{ items: [ \"fork\", \"knife\" ] }", - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "has 2 elements instead of 3 at $.items" - }; - yield return new object[] - { - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "{ items: [ \"fork\", \"spoon\", \"knife\" ] }", - "has a different value at $.items[1]" - }; - yield return new object[] - { - "{ tree: { } }", - "{ tree: \"oak\" }", - "has an object instead of a string at $.tree" - }; - yield return new object[] - { - "{ tree: { leaves: 10} }", - "{ tree: { branches: 5, leaves: 10 } }", - "misses property $.tree.branches" - }; - yield return new object[] - { - "{ tree: { branches: 5, leaves: 10 } }", - "{ tree: { leaves: 10} }", - "has extra property $.tree.branches" - }; - yield return new object[] - { - "{ tree: { leaves: 5 } }", - "{ tree: { leaves: 10} }", - "has a different value at $.tree.leaves" - }; - yield return new object[] - { - "{ eyes: \"blue\" }", - "{ eyes: [] }", - "has a string instead of an array at $.eyes" - }; - yield return new object[] - { - "{ eyes: \"blue\" }", - "{ eyes: 2 }", - "has a string instead of an integer at $.eyes" - }; - yield return new object[] - { - "{ id: 1 }", - "{ id: 2 }", - "has a different value at $.id" - }; + 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))] @@ -862,108 +846,89 @@ public void When_array_elements_are_matching_within_a_nested_structure_it_should act.Should().NotThrow(); } - public static IEnumerable FailingContainSubtreeCases + public static TheoryData FailingContainSubtreeCases => new() { - get { - yield return new object[] - { - null, - "{ id: 2 }", - "is null" - }; - yield return new object[] - { - "{ id: 1 }", - null, - "is not null" - }; - yield return new object[] - { - "{ foo: 'foo', bar: 'bar' }", - "{ baz: 'baz' }", - "misses property $.baz" - }; - yield return new object[] - { - "{ items: [] }", - "{ items: 2 }", - "has an array instead of an integer at $.items" - }; - yield return new object[] - { - "{ items: [ \"fork\", \"knife\" ] }", - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "misses expected element $.items[2]" - }; - yield return new object[] - { - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "{ items: [ \"fork\", \"spoon\", \"knife\" ] }", - "has expected element $.items[2] in the wrong order" - }; - yield return new object[] - { - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "{ items: [ \"fork\", \"fork\" ] }", - "has a different value at $.items[1]" - }; - yield return new object[] - { - "{ tree: { } }", - "{ tree: \"oak\" }", - "has an object instead of a string at $.tree" - }; - yield return new object[] - { - "{ tree: { leaves: 10} }", - "{ tree: { branches: 5, leaves: 10 } }", - "misses property $.tree.branches" - }; - yield return new object[] - { - "{ tree: { leaves: 5 } }", - "{ tree: { leaves: 10} }", - "has a different value at $.tree.leaves" - }; - yield return new object[] - { - "{ eyes: \"blue\" }", - "{ eyes: [] }", - "has a string instead of an array at $.eyes" - }; - yield return new object[] - { - "{ eyes: \"blue\" }", - "{ eyes: 2 }", - "has a string instead of an integer at $.eyes" - }; - yield return new object[] - { - "{ id: 1 }", - "{ id: 2 }", - "has a different value at $.id" - }; - yield return new object[] - { - "{ items: [ { id: 1 }, { id: 3 }, { id: 5 } ] }", - "{ items: [ { id: 1 }, { id: 2 } ] }", - "has a different value at $.items[1].id" - }; - yield return new object[] - { - "{ foo: '1' }", - "{ foo: 1 }", - "has a string instead of an integer at $.foo" - }; - yield return new object[] - { - "{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } } }", - "{ child: { grandchild: { tag: 'ooops' } } }", - "has a different value at $.child.grandchild.tag" - }; + 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))] diff --git a/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs b/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs index 96f2881..0c6a5fa 100644 --- a/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs +++ b/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs @@ -1,5 +1,4 @@ using System; -using AutoFixture; using FluentAssertions; using FluentAssertions.Json; using FluentAssertions.Json.Specs.Models; @@ -12,18 +11,23 @@ namespace SomeOtherNamespace // ReSharper restore CheckNamespace public class ShouldBeJsonSerializableTests { - private readonly Fixture fixture; - - public ShouldBeJsonSerializableTests() - { - fixture = new Fixture(); - } - [Fact] public void Simple_poco_should_be_serializable() { // arrange - var target = fixture.Create(); + var target = new SimplePocoWithPrimitiveTypes + { + Id = 1, + GlobalId = Guid.NewGuid(), + Name = "Name", + DateOfBirth = DateTime.UtcNow, + Height = 1, + Weight = 1, + ShoeSize = 1, + IsActive = true, + Image = new[] { (byte)1 }, + Category = '1' + }; // act Action act = () => target.Should().BeJsonSerializable(); @@ -36,7 +40,21 @@ public void Simple_poco_should_be_serializable() public void Complex_poco_should_be_serializable() { // arrange - var target = fixture.Create(); + 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(); @@ -50,7 +68,7 @@ public void Class_that_does_not_have_default_constructor_should_not_be_serializa { // arrange const string reasonText = "this is the reason"; - var target = fixture.Create(); + var target = new PocoWithNoDefaultConstructor(1); // act Action act = () => target.Should().BeJsonSerializable(reasonText); @@ -69,7 +87,11 @@ public void Class_that_has_ignored_property_should_not_be_serializable_if_equiva { // arrange const string reasonText = "this is the reason"; - var target = fixture.Create(); + var target = new PocoWithIgnoredProperty + { + Id = 1, + Name = "Name", + }; // act Action act = () => target.Should().BeJsonSerializable(reasonText); @@ -87,7 +109,11 @@ public void Class_that_has_ignored_property_should_not_be_serializable_if_equiva public void Class_that_has_ignored_property_should_be_serializable_when_equivalency_options_are_configured() { // arrange - var target = fixture.Create(); + var target = new PocoWithIgnoredProperty + { + Id = 1, + Name = "Name", + }; // act Action act = () => target.Should().BeJsonSerializable(opts => opts.Excluding(p => p.Name)); @@ -131,7 +157,13 @@ public void Should_fail_when_subject_is_not_same_type_as_the_specified_generic_t public void Should_fail_when_derived_type_is_not_serializable_when_presented_as_base_class() { // arrange - AddressDto target = fixture.Create(); + AddressDto target = new DerivedFromAddressDto + { + AddressLine1 = "AddressLine1", + AddressLine2 = "AddressLine2", + AddressLine3 = "AddressLine3", + LastUpdated = DateTime.UtcNow, + }; // act Action act = () => target.Should().BeJsonSerializable(); diff --git a/global.json b/global.json index b94f8d2..368a14e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "5.0.200", + "version": "8.0.100", "rollForward": "latestMajor" } }