From a02c47af8180ad0b424e8bc1d10c698d28799841 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 20 Dec 2024 17:12:00 +0200 Subject: [PATCH] [Fusion] Improved composition errors --- .../Logging/CompositionLog.cs | 2 +- .../Logging/Contracts/ICompositionLog.cs | 2 +- .../Logging/LogEntryHelper.cs | 93 +++++++++++++------ .../Rules/ExternalMissingOnBaseRule.cs | 18 ++-- .../Rules/OutputFieldTypesMergeableRule.cs | 19 +++- .../CompositionResources.Designer.cs | 81 ++++++++++++---- .../Properties/CompositionResources.resx | 14 +-- .../Fusion.Composition/ValidationHelper.cs | 17 ---- .../CompositionTestBase.cs | 23 +++++ ...DisallowedInaccessibleElementsRuleTests.cs | 49 ++++++---- ...xternalArgumentDefaultMismatchRuleTests.cs | 53 +++++++---- .../Rules/ExternalMissingOnBaseRuleTests.cs | 43 +++++---- .../OutputFieldTypesMergeableRuleTests.cs | 67 +++++++++---- 13 files changed, 327 insertions(+), 154 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/CompositionTestBase.cs diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs index 3f51bdfed5f..91322ae3e20 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs @@ -3,7 +3,7 @@ namespace HotChocolate.Fusion.Logging; -public sealed class CompositionLog : ICompositionLog, IEnumerable +public sealed class CompositionLog : ICompositionLog { public bool HasErrors { get; private set; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs index 179cd3971a7..58501fe5d53 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs @@ -3,7 +3,7 @@ namespace HotChocolate.Fusion.Logging.Contracts; /// /// Defines an interface for logging composition information, warnings, and errors. /// -public interface ICompositionLog +public interface ICompositionLog : IEnumerable { /// /// Gets a value indicating whether the log contains errors. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs index ae4ded0c857..6e14b99ddf5 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs @@ -8,39 +8,53 @@ internal static class LogEntryHelper public static LogEntry DisallowedInaccessibleBuiltInScalar( ScalarTypeDefinition scalar, SchemaDefinition schema) - => new( - string.Format(LogEntryHelper_DisallowedInaccessibleBuiltInScalar, scalar.Name), + { + return new LogEntry( + string.Format( + LogEntryHelper_DisallowedInaccessibleBuiltInScalar, + scalar.Name, + schema.Name), LogEntryCodes.DisallowedInaccessible, LogSeverity.Error, new SchemaCoordinate(scalar.Name), scalar, schema); + } public static LogEntry DisallowedInaccessibleIntrospectionType( INamedTypeDefinition type, SchemaDefinition schema) - => new( - string.Format(LogEntryHelper_DisallowedInaccessibleIntrospectionType, type.Name), + { + return new LogEntry( + string.Format( + LogEntryHelper_DisallowedInaccessibleIntrospectionType, + type.Name, + schema.Name), LogEntryCodes.DisallowedInaccessible, LogSeverity.Error, new SchemaCoordinate(type.Name), type, schema); + } public static LogEntry DisallowedInaccessibleIntrospectionField( OutputFieldDefinition field, string typeName, SchemaDefinition schema) - => new( + { + var coordinate = new SchemaCoordinate(typeName, field.Name); + + return new LogEntry( string.Format( LogEntryHelper_DisallowedInaccessibleIntrospectionField, - field.Name, - typeName), + coordinate, + schema.Name), LogEntryCodes.DisallowedInaccessible, LogSeverity.Error, - new SchemaCoordinate(typeName, field.Name), + coordinate, field, schema); + } public static LogEntry DisallowedInaccessibleIntrospectionArgument( InputFieldDefinition argument, @@ -53,8 +67,8 @@ public static LogEntry DisallowedInaccessibleIntrospectionArgument( return new LogEntry( string.Format( LogEntryHelper_DisallowedInaccessibleIntrospectionArgument, - argument.Name, - coordinate), + coordinate, + schema.Name), LogEntryCodes.DisallowedInaccessible, LogSeverity.Error, coordinate, @@ -66,15 +80,23 @@ public static LogEntry DisallowedInaccessibleDirectiveArgument( InputFieldDefinition argument, string directiveName, SchemaDefinition schema) - => new( + { + var coordinate = new SchemaCoordinate( + directiveName, + argumentName: argument.Name, + ofDirective: true); + + return new LogEntry( string.Format( LogEntryHelper_DisallowedInaccessibleDirectiveArgument, - argument.Name, - directiveName), + coordinate, + schema.Name), LogEntryCodes.DisallowedInaccessible, LogSeverity.Error, - new SchemaCoordinate(directiveName, argumentName: argument.Name, ofDirective: true), - schema: schema); + coordinate, + argument, + schema); + } public static LogEntry ExternalArgumentDefaultMismatch( string argumentName, @@ -90,23 +112,40 @@ public static LogEntry ExternalArgumentDefaultMismatch( coordinate); } - public static LogEntry ExternalMissingOnBase(string fieldName, string typeName) - => new( - string.Format( - LogEntryHelper_ExternalMissingOnBase, - fieldName, - typeName), + public static LogEntry ExternalMissingOnBase( + OutputFieldDefinition externalField, + INamedTypeDefinition type, + SchemaDefinition schema) + { + var coordinate = new SchemaCoordinate(type.Name, externalField.Name); + + return new LogEntry( + string.Format(LogEntryHelper_ExternalMissingOnBase, coordinate, schema.Name), LogEntryCodes.ExternalMissingOnBase, LogSeverity.Error, - new SchemaCoordinate(typeName, fieldName)); + coordinate, + externalField, + schema); + } + + public static LogEntry OutputFieldTypesNotMergeable( + OutputFieldDefinition field, + string typeName, + SchemaDefinition schemaA, + SchemaDefinition schemaB) + { + var coordinate = new SchemaCoordinate(typeName, field.Name); - public static LogEntry OutputFieldTypesNotMergeable(string fieldName, string typeName) - => new( + return new LogEntry( string.Format( LogEntryHelper_OutputFieldTypesNotMergeable, - fieldName, - typeName), + coordinate, + schemaA.Name, + schemaB.Name), LogEntryCodes.OutputFieldTypesNotMergeable, LogSeverity.Error, - new SchemaCoordinate(typeName, fieldName)); + coordinate, + field, + schemaA); + } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/ExternalMissingOnBaseRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/ExternalMissingOnBaseRule.cs index 97db2fa4961..46a9dd2308b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/ExternalMissingOnBaseRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/ExternalMissingOnBaseRule.cs @@ -1,5 +1,5 @@ +using System.Collections.Immutable; using HotChocolate.Fusion.Events; -using HotChocolate.Fusion.Extensions; using static HotChocolate.Fusion.Logging.LogEntryHelper; namespace HotChocolate.Fusion.PreMergeValidation.Rules; @@ -17,14 +17,20 @@ internal sealed class ExternalMissingOnBaseRule : IEventHandler ValidationHelper.IsExternal(i.Field)); - var nonExternalFieldCount = fieldGroup.Length - externalFieldCount; + var externalFields = fieldGroup + .Where(i => ValidationHelper.IsExternal(i.Field)) + .ToImmutableArray(); - if (externalFieldCount != 0 && nonExternalFieldCount == 0) + var nonExternalFieldCount = fieldGroup.Length - externalFields.Length; + + foreach (var (field, type, schema) in externalFields) { - context.Log.Write(ExternalMissingOnBase(fieldName, typeName)); + if (nonExternalFieldCount == 0) + { + context.Log.Write(ExternalMissingOnBase(field, type, schema)); + } } } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs index f85f44b8aef..b8149a91890 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs @@ -14,11 +14,24 @@ internal sealed class OutputFieldTypesMergeableRule : IEventHandler i.Field)])) + for (var i = 0; i < fieldGroup.Length - 1; i++) { - context.Log.Write(OutputFieldTypesNotMergeable(fieldName, typeName)); + var fieldInfoA = fieldGroup[i]; + var fieldInfoB = fieldGroup[i + 1]; + var typeA = fieldInfoA.Field.Type; + var typeB = fieldInfoB.Field.Type; + + if (!ValidationHelper.SameTypeShape(typeA, typeB)) + { + context.Log.Write( + OutputFieldTypesNotMergeable( + fieldInfoA.Field, + typeName, + fieldInfoA.Schema, + fieldInfoB.Schema)); + } } } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs index 3da2729fa65..a36dca256ca 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs @@ -11,32 +11,46 @@ namespace HotChocolate.Fusion.Properties { using System; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class CompositionResources { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal CompositionResources() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("HotChocolate.Fusion.Properties.CompositionResources", typeof(CompositionResources).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HotChocolate.Fusion.Properties.CompositionResources", typeof(CompositionResources).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -45,54 +59,81 @@ internal static System.Globalization.CultureInfo Culture { } } + /// + /// Looks up a localized string similar to Pre-merge validation failed. View the composition log for details.. + /// internal static string ErrorHelper_PreMergeValidationFailed { get { return ResourceManager.GetString("ErrorHelper_PreMergeValidationFailed", resourceCulture); } } + /// + /// Looks up a localized string similar to The built-in scalar type '{0}' in schema '{1}' is not accessible.. + /// internal static string LogEntryHelper_DisallowedInaccessibleBuiltInScalar { get { return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleBuiltInScalar", resourceCulture); } } - internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionType { + /// + /// Looks up a localized string similar to The built-in directive argument '{0}' in schema '{1}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleDirectiveArgument { get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionType", resourceCulture); + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleDirectiveArgument", resourceCulture); } } - internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionField { + /// + /// Looks up a localized string similar to The introspection argument '{0}' in schema '{1}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionArgument { get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionField", resourceCulture); + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionArgument", resourceCulture); } } - internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionArgument { + /// + /// Looks up a localized string similar to The introspection field '{0}' in schema '{1}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionField { get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionArgument", resourceCulture); + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionField", resourceCulture); } } - internal static string LogEntryHelper_DisallowedInaccessibleDirectiveArgument { + /// + /// Looks up a localized string similar to The introspection type '{0}' in schema '{1}' is not accessible.. + /// + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionType { get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleDirectiveArgument", resourceCulture); + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionType", resourceCulture); } } + /// + /// Looks up a localized string similar to The argument with schema coordinate '{0}' has inconsistent default values.. + /// internal static string LogEntryHelper_ExternalArgumentDefaultMismatch { get { return ResourceManager.GetString("LogEntryHelper_ExternalArgumentDefaultMismatch", resourceCulture); } } + /// + /// Looks up a localized string similar to External field '{0}' in schema '{1}' is not defined (non-external) in any other schema.. + /// internal static string LogEntryHelper_ExternalMissingOnBase { get { return ResourceManager.GetString("LogEntryHelper_ExternalMissingOnBase", resourceCulture); } } + /// + /// Looks up a localized string similar to Field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.. + /// internal static string LogEntryHelper_OutputFieldTypesNotMergeable { get { return ResourceManager.GetString("LogEntryHelper_OutputFieldTypesNotMergeable", resourceCulture); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx index e9249fef475..df1b4ed6217 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx @@ -22,27 +22,27 @@ Pre-merge validation failed. View the composition log for details. - The built-in scalar type '{0}' is not accessible. + The built-in scalar type '{0}' in schema '{1}' is not accessible. - The introspection type '{0}' is not accessible. + The introspection type '{0}' in schema '{1}' is not accessible. - The introspection field '{0}' on type '{1}' is not accessible. + The introspection field '{0}' in schema '{1}' is not accessible. - The introspection argument '{0}' with schema coordinate '{1}' is not accessible. + The introspection argument '{0}' in schema '{1}' is not accessible. - The argument '{0}' on built-in directive type '{1}' is not accessible. + The built-in directive argument '{0}' in schema '{1}' is not accessible. The argument with schema coordinate '{0}' has inconsistent default values. - Field '{0}' on type '{1}' is only declared as external. + External field '{0}' in schema '{1}' is not defined (non-external) in any other schema. - Field '{0}' on type '{1}' is not mergeable. + Field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs index 239b4373460..b84d9c171ba 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs @@ -1,26 +1,9 @@ -using System.Collections.Immutable; using HotChocolate.Skimmed; namespace HotChocolate.Fusion; internal sealed class ValidationHelper { - public static bool FieldsAreMergeable(ImmutableArray fields) - { - for (var i = 0; i < fields.Length - 1; i++) - { - var typeA = fields[i].Type; - var typeB = fields[i + 1].Type; - - if (!SameTypeShape(typeA, typeB)) - { - return false; - } - } - - return true; - } - public static bool IsAccessible(IDirectivesProvider type) { return !type.Directives.ContainsName(WellKnownDirectiveNames.Inaccessible); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/CompositionTestBase.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/CompositionTestBase.cs new file mode 100644 index 00000000000..ee2d3e04f48 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/CompositionTestBase.cs @@ -0,0 +1,23 @@ +using HotChocolate.Fusion; +using HotChocolate.Fusion.Logging; +using HotChocolate.Skimmed.Serialization; + +namespace HotChocolate.Composition; + +public abstract class CompositionTestBase +{ + internal static CompositionContext CreateCompositionContext(string[] sdl) + { + return new CompositionContext( + [ + .. sdl.Select((s, i) => + { + var schemaDefinition = SchemaParser.Parse(s); + schemaDefinition.Name = ((char)('A' + i)).ToString(); + + return schemaDefinition; + }) + ], + new CompositionLog()); + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs index c5e9b551786..836fe1ba1ac 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs @@ -1,47 +1,44 @@ -using HotChocolate.Fusion; using HotChocolate.Fusion.Logging; using HotChocolate.Fusion.PreMergeValidation; using HotChocolate.Fusion.PreMergeValidation.Rules; -using HotChocolate.Skimmed.Serialization; namespace HotChocolate.Composition.PreMergeValidation.Rules; -public sealed class DisallowedInaccessibleElementsRuleTests +public sealed class DisallowedInaccessibleElementsRuleTests : CompositionTestBase { + private readonly PreMergeValidator _preMergeValidator = + new([new DisallowedInaccessibleElementsRule()]); + [Theory] [MemberData(nameof(ValidExamplesData))] public void Examples_Valid(string[] sdl) { // arrange - var log = new CompositionLog(); - var context = new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log); - var preMergeValidator = new PreMergeValidator([new DisallowedInaccessibleElementsRule()]); + var context = CreateCompositionContext(sdl); // act - var result = preMergeValidator.Validate(context); + var result = _preMergeValidator.Validate(context); // assert Assert.True(result.IsSuccess); - Assert.True(log.IsEmpty); + Assert.True(context.Log.IsEmpty); } [Theory] [MemberData(nameof(InvalidExamplesData))] - public void Examples_Invalid(string[] sdl) + public void Examples_Invalid(string[] sdl, string[] errorMessages) { // arrange - var log = new CompositionLog(); - var context = new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log); - var preMergeValidator = new PreMergeValidator([new DisallowedInaccessibleElementsRule()]); + var context = CreateCompositionContext(sdl); // act - var result = preMergeValidator.Validate(context); + var result = _preMergeValidator.Validate(context); // assert Assert.True(result.IsFailure); - Assert.Single(log); - Assert.Equal("DISALLOWED_INACCESSIBLE", log.First().Code); - Assert.Equal(LogSeverity.Error, log.First().Severity); + Assert.Equal(errorMessages, context.Log.Select(e => e.Message).ToArray()); + Assert.True(context.Log.All(e => e.Code == "DISALLOWED_INACCESSIBLE")); + Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error)); } public static TheoryData ValidExamplesData() @@ -62,9 +59,9 @@ type Product { }; } - public static TheoryData InvalidExamplesData() + public static TheoryData InvalidExamplesData() { - return new TheoryData + return new TheoryData { // In this example, the String scalar is marked as @inaccessible. This violates the rule // because String is a required built-in type that cannot be inaccessible. @@ -78,6 +75,9 @@ type Product { name: String } """ + ], + [ + "The built-in scalar type 'String' in schema 'A' is not accessible." ] }, // In this example, the introspection type __Type is marked as @inaccessible. This @@ -92,6 +92,9 @@ type __Type @inaccessible { fields(includeDeprecated: Boolean = false): [__Field!] } """ + ], + [ + "The introspection type '__Type' in schema 'A' is not accessible." ] }, // Inaccessible introspection field. @@ -104,6 +107,9 @@ type __Type { fields(includeDeprecated: Boolean = false): [__Field!] } """ + ], + [ + "The introspection field '__Type.kind' in schema 'A' is not accessible." ] }, // Inaccessible introspection argument. @@ -116,6 +122,10 @@ type __Type { fields(includeDeprecated: Boolean = false @inaccessible): [__Field!] } """ + ], + [ + "The introspection argument '__Type.fields(includeDeprecated:)' in schema " + + "'A' is not accessible." ] }, // Inaccessible built-in directive argument. @@ -125,6 +135,9 @@ type __Type { directive @skip(if: Boolean! @inaccessible) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT """ + ], + [ + "The built-in directive argument '@skip(if:)' in schema 'A' is not accessible." ] } }; diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/ExternalArgumentDefaultMismatchRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/ExternalArgumentDefaultMismatchRuleTests.cs index 397ef2cf576..0185ad2a776 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/ExternalArgumentDefaultMismatchRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/ExternalArgumentDefaultMismatchRuleTests.cs @@ -1,47 +1,44 @@ -using HotChocolate.Fusion; using HotChocolate.Fusion.Logging; using HotChocolate.Fusion.PreMergeValidation; using HotChocolate.Fusion.PreMergeValidation.Rules; -using HotChocolate.Skimmed.Serialization; namespace HotChocolate.Composition.PreMergeValidation.Rules; -public sealed class ExternalArgumentDefaultMismatchRuleTests +public sealed class ExternalArgumentDefaultMismatchRuleTests : CompositionTestBase { + private readonly PreMergeValidator _preMergeValidator = + new([new ExternalArgumentDefaultMismatchRule()]); + [Theory] [MemberData(nameof(ValidExamplesData))] public void Examples_Valid(string[] sdl) { // arrange - var log = new CompositionLog(); - var context = new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log); - var preMergeValidator = new PreMergeValidator([new ExternalArgumentDefaultMismatchRule()]); + var context = CreateCompositionContext(sdl); // act - var result = preMergeValidator.Validate(context); + var result = _preMergeValidator.Validate(context); // assert Assert.True(result.IsSuccess); - Assert.True(log.IsEmpty); + Assert.True(context.Log.IsEmpty); } [Theory] [MemberData(nameof(InvalidExamplesData))] - public void Examples_Invalid(string[] sdl) + public void Examples_Invalid(string[] sdl, string[] errorMessages) { // arrange - var log = new CompositionLog(); - var context = new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log); - var preMergeValidator = new PreMergeValidator([new ExternalArgumentDefaultMismatchRule()]); + var context = CreateCompositionContext(sdl); // act - var result = preMergeValidator.Validate(context); + var result = _preMergeValidator.Validate(context); // assert Assert.True(result.IsFailure); - Assert.Single(log); - Assert.Equal("EXTERNAL_ARGUMENT_DEFAULT_MISMATCH", log.First().Code); - Assert.Equal(LogSeverity.Error, log.First().Severity); + Assert.Equal(errorMessages, context.Log.Select(e => e.Message).ToArray()); + Assert.True(context.Log.All(e => e.Code == "EXTERNAL_ARGUMENT_DEFAULT_MISMATCH")); + Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error)); } public static TheoryData ValidExamplesData() @@ -89,9 +86,9 @@ type Product { }; } - public static TheoryData InvalidExamplesData() + public static TheoryData InvalidExamplesData() { - return new TheoryData + return new TheoryData { // Here, the `name` field on Product is defined in one source schema and marked as // @external in another. The argument `language` has different default values in the @@ -108,6 +105,10 @@ type Product { name(language: String = "de"): String @external } """ + ], + [ + "The argument with schema coordinate 'Product.name(language:)' has " + + "inconsistent default values." ] }, // In the following example, the `name` field on Product is defined in one source schema @@ -126,6 +127,10 @@ type Product { name(language: String): String @external } """ + ], + [ + "The argument with schema coordinate 'Product.name(language:)' has " + + "inconsistent default values." ] }, // Here, the `name` field on Product is defined without a default value in the @@ -142,6 +147,10 @@ type Product { name(language: String = "en"): String @external } """ + ], + [ + "The argument with schema coordinate 'Product.name(language:)' has " + + "inconsistent default values." ] }, // Here, the `name` field on Product is defined with multiple arguments. One argument @@ -158,6 +167,10 @@ type Product { name(language: String = "en", localization: String = "sa"): String @external } """ + ], + [ + "The argument with schema coordinate 'Product.name(localization:)' has " + + "inconsistent default values." ] }, // Here, the `name` field on Product is defined with multiple arguments. One argument @@ -175,6 +188,10 @@ type Product { name(language: String = "en", localization: String): String @external } """ + ], + [ + "The argument with schema coordinate 'Product.name(localization:)' has " + + "inconsistent default values." ] } }; diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/ExternalMissingOnBaseRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/ExternalMissingOnBaseRuleTests.cs index 425f8d45fef..bb8c93a2314 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/ExternalMissingOnBaseRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/ExternalMissingOnBaseRuleTests.cs @@ -1,47 +1,44 @@ -using HotChocolate.Fusion; using HotChocolate.Fusion.Logging; using HotChocolate.Fusion.PreMergeValidation; using HotChocolate.Fusion.PreMergeValidation.Rules; -using HotChocolate.Skimmed.Serialization; namespace HotChocolate.Composition.PreMergeValidation.Rules; -public sealed class ExternalMissingOnBaseRuleTests +public sealed class ExternalMissingOnBaseRuleTests : CompositionTestBase { + private readonly PreMergeValidator _preMergeValidator = + new([new ExternalMissingOnBaseRule()]); + [Theory] [MemberData(nameof(ValidExamplesData))] public void Examples_Valid(string[] sdl) { // arrange - var log = new CompositionLog(); - var context = new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log); - var preMergeValidator = new PreMergeValidator([new ExternalMissingOnBaseRule()]); + var context = CreateCompositionContext(sdl); // act - var result = preMergeValidator.Validate(context); + var result = _preMergeValidator.Validate(context); // assert Assert.True(result.IsSuccess); - Assert.True(log.IsEmpty); + Assert.True(context.Log.IsEmpty); } [Theory] [MemberData(nameof(InvalidExamplesData))] - public void Examples_Invalid(string[] sdl) + public void Examples_Invalid(string[] sdl, string[] errorMessages) { // arrange - var log = new CompositionLog(); - var context = new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log); - var preMergeValidator = new PreMergeValidator([new ExternalMissingOnBaseRule()]); + var context = CreateCompositionContext(sdl); // act - var result = preMergeValidator.Validate(context); + var result = _preMergeValidator.Validate(context); // assert Assert.True(result.IsFailure); - Assert.Single(log); - Assert.Equal("EXTERNAL_MISSING_ON_BASE", log.First().Code); - Assert.Equal(LogSeverity.Error, log.First().Severity); + Assert.Equal(errorMessages, context.Log.Select(e => e.Message).ToArray()); + Assert.True(context.Log.All(e => e.Code == "EXTERNAL_MISSING_ON_BASE")); + Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error)); } public static TheoryData ValidExamplesData() @@ -72,9 +69,9 @@ type Product { }; } - public static TheoryData InvalidExamplesData() + public static TheoryData InvalidExamplesData() { - return new TheoryData + return new TheoryData { // In this example, the `name` field on Product is marked as @external in source schema // B but has no non-@external declaration in any other source schema, violating the @@ -94,6 +91,10 @@ type Product { name: String @external } """ + ], + [ + "External field 'Product.name' in schema 'B' is not defined (non-external) " + + "in any other schema." ] }, // The `name` field is external in both source schemas. @@ -113,6 +114,12 @@ type Product { name: String @external } """ + ], + [ + "External field 'Product.name' in schema 'A' is not defined (non-external) " + + "in any other schema.", + "External field 'Product.name' in schema 'B' is not defined (non-external) " + + "in any other schema." ] } }; diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs index 9f8d7001c91..c9c79c62dfc 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs @@ -1,47 +1,44 @@ -using HotChocolate.Fusion; using HotChocolate.Fusion.Logging; using HotChocolate.Fusion.PreMergeValidation; using HotChocolate.Fusion.PreMergeValidation.Rules; -using HotChocolate.Skimmed.Serialization; namespace HotChocolate.Composition.PreMergeValidation.Rules; -public sealed class OutputFieldTypesMergeableRuleTests +public sealed class OutputFieldTypesMergeableRuleTests : CompositionTestBase { + private readonly PreMergeValidator _preMergeValidator = + new([new OutputFieldTypesMergeableRule()]); + [Theory] [MemberData(nameof(ValidExamplesData))] public void Examples_Valid(string[] sdl) { // arrange - var log = new CompositionLog(); - var context = new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log); - var preMergeValidator = new PreMergeValidator([new OutputFieldTypesMergeableRule()]); + var context = CreateCompositionContext(sdl); // act - var result = preMergeValidator.Validate(context); + var result = _preMergeValidator.Validate(context); // assert Assert.True(result.IsSuccess); - Assert.True(log.IsEmpty); + Assert.True(context.Log.IsEmpty); } [Theory] [MemberData(nameof(InvalidExamplesData))] - public void Examples_Invalid(string[] sdl) + public void Examples_Invalid(string[] sdl, string[] errorMessages) { // arrange - var log = new CompositionLog(); - var context = new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log); - var preMergeValidator = new PreMergeValidator([new OutputFieldTypesMergeableRule()]); + var context = CreateCompositionContext(sdl); // act - var result = preMergeValidator.Validate(context); + var result = _preMergeValidator.Validate(context); // assert Assert.True(result.IsFailure); - Assert.Single(log); - Assert.Equal("OUTPUT_FIELD_TYPES_NOT_MERGEABLE", log.First().Code); - Assert.Equal(LogSeverity.Error, log.First().Severity); + Assert.Equal(errorMessages, context.Log.Select(e => e.Message).ToArray()); + Assert.True(context.Log.All(e => e.Code == "OUTPUT_FIELD_TYPES_NOT_MERGEABLE")); + Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error)); } public static TheoryData ValidExamplesData() @@ -101,9 +98,9 @@ type User { }; } - public static TheoryData InvalidExamplesData() + public static TheoryData InvalidExamplesData() { - return new TheoryData + return new TheoryData { // Fields are not mergeable if the named types are different in kind or name. { @@ -118,6 +115,10 @@ type User { birthdate: DateTime! } """ + ], + [ + "Field 'User.birthdate' has a different type shape in schema 'A' than it " + + "does in schema 'B'." ] }, { @@ -138,6 +139,36 @@ type User { scalar Tag """ + ], + [ + "Field 'User.tags' has a different type shape in schema 'A' than it does in " + + "schema 'B'." + ] + }, + // More than two schemas. + { + [ + """ + type User { + birthdate: String! + } + """, + """ + type User { + birthdate: DateTime! + } + """, + """ + type User { + birthdate: Int! + } + """ + ], + [ + "Field 'User.birthdate' has a different type shape in schema 'A' than it " + + "does in schema 'B'.", + "Field 'User.birthdate' has a different type shape in schema 'B' than it " + + "does in schema 'C'." ] } };