From 665e00f527100a14b7eedd50b788332de4a3f30d Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 9 Aug 2023 11:45:06 -0500 Subject: [PATCH] Add support for suppressions to smithy-diff Closes #1861 --- smithy-diff/README.md | 3 + .../amazon/smithy/diff/ModelDiff.java | 11 + .../amazon/smithy/diff/ModelDiffTest.java | 29 +++ .../amazon/smithy/diff/suppressions-a.smithy | 16 ++ .../amazon/smithy/diff/suppressions-b.smithy | 16 ++ .../smithy/model/loader/ModelValidator.java | 127 +++-------- .../validation/ValidationEventDecorator.java | 26 ++- .../ModelBasedEventDecorator.java | 199 ++++++++++++++++++ .../ModelBasedEventDecoratorTest.java | 95 +++++++++ .../suppressions/bad-severityOverrides.smithy | 7 + .../suppressions/bad-suppressions.smithy | 7 + 11 files changed, 425 insertions(+), 111 deletions(-) create mode 100644 smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy create mode 100644 smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java create mode 100644 smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy diff --git a/smithy-diff/README.md b/smithy-diff/README.md index 98c86a7c912..4ad5dd42baf 100644 --- a/smithy-diff/README.md +++ b/smithy-diff/README.md @@ -14,6 +14,9 @@ Model modelB = loadModelB(); List events = ModelDiff.compare(modelA, modelB); ``` +Smithy Diff will load and apply metadata suppressions and severityOverrides +from the new model to any emitted diff events. + # Adding a custom DiffEvaluator This library finds all instances of `DiffEvaluator` diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java index 3e18a774d95..cb5c6b88199 100644 --- a/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java @@ -27,6 +27,8 @@ import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.ValidationEventDecorator; +import software.amazon.smithy.model.validation.suppressions.ModelBasedEventDecorator; import software.amazon.smithy.utils.SmithyBuilder; /** @@ -280,8 +282,17 @@ public Result compare() { List evaluators = new ArrayList<>(); ServiceLoader.load(DiffEvaluator.class, classLoader).forEach(evaluators::add); Differences differences = Differences.detect(oldModel, newModel); + + // Applies suppressions and elevates event severities. + ValidationEventDecorator decoratorResult = new ModelBasedEventDecorator() + .createDecorator(newModel) + .getResult() + .orElse(ValidationEventDecorator.IDENTITY); + List diffEvents = evaluators.parallelStream() .flatMap(evaluator -> evaluator.evaluate(differences).stream()) + // No need to call canDecorate first since that method will always return true in any code path. + .map(decoratorResult::decorate) .collect(Collectors.toList()); return new Result(differences, diffEvents, oldModelEvents, newModelEvents); diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java index ab6b72d0f24..cbc22a4169a 100644 --- a/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java +++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java @@ -94,4 +94,33 @@ public void detectsWhenNoBreakingChanges() { assertThat(result.isDiffBreaking(), is(false)); } + + @Test + public void appliesSuppressionsToDiff() { + Model oldModel = Model.assembler() + .addImport(getClass().getResource("suppressions-a.smithy")) + .assemble() + .unwrap(); + Model newModel = Model.assembler() + .addImport(getClass().getResource("suppressions-b.smithy")) + .assemble() + .unwrap(); + + ModelDiff.Result result = ModelDiff.builder() + .oldModel(oldModel) + .newModel(newModel) + .compare(); + + assertThat(result.isDiffBreaking(), is(false)); + + boolean found = false; + for (ValidationEvent event : result.getDiffEvents()) { + if (event.getId().equals("ChangedMemberOrder")) { + assertThat(event.getSeverity(), equalTo(Severity.SUPPRESSED)); + found = true; + } + } + + assertThat(found, is(true)); + } } diff --git a/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy new file mode 100644 index 00000000000..4095cadf743 --- /dev/null +++ b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy @@ -0,0 +1,16 @@ +$version: "2.0" + +metadata suppressions = [ + { + id: "ChangedMemberOrder" + namespace: "smithy.example" + } +] + +namespace smithy.example + +structure Foo { + a: String + b: String + c: String +} diff --git a/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy new file mode 100644 index 00000000000..b08378e4eed --- /dev/null +++ b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy @@ -0,0 +1,16 @@ +$version: "2.0" + +metadata suppressions = [ + { + id: "ChangedMemberOrder" + namespace: "smithy.example" + } +] + +namespace smithy.example + +structure Foo { + c: String + a: String + b: String +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java index af9284f5251..85edc2c2031 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java @@ -24,22 +24,18 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.SourceLocation; -import software.amazon.smithy.model.node.ObjectNode; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.traits.SuppressTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.model.validation.ValidationEventDecorator; import software.amazon.smithy.model.validation.Validator; import software.amazon.smithy.model.validation.ValidatorFactory; +import software.amazon.smithy.model.validation.suppressions.ModelBasedEventDecorator; import software.amazon.smithy.model.validation.suppressions.SeverityOverride; -import software.amazon.smithy.model.validation.suppressions.Suppression; import software.amazon.smithy.model.validation.validators.ResourceCycleValidator; import software.amazon.smithy.model.validation.validators.TargetValidator; +import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.SetUtils; import software.amazon.smithy.utils.SmithyBuilder; @@ -65,9 +61,6 @@ */ final class ModelValidator implements Validator { - private static final String SUPPRESSIONS = "suppressions"; - private static final String SEVERITY_OVERRIDES = "severityOverrides"; - // Lazy initialization holder class idiom to hold a default validator factory. private static final class LazyValidatorFactoryHolder { static final ValidatorFactory INSTANCE = ValidatorFactory.createServiceFactory( @@ -194,7 +187,6 @@ public ModelValidator build() { private static final class LoadedModelValidator { private final Model model; - private final List suppressions = new ArrayList<>(); private final List severityOverrides; private final List validators; private final List events = new ArrayList<>(); @@ -203,56 +195,38 @@ private static final class LoadedModelValidator { private LoadedModelValidator(Model model, ModelValidator validator) { this.model = model; - this.validationEventDecorator = validator.validationEventDecorator; this.eventListener = validator.eventListener; this.severityOverrides = new ArrayList<>(validator.severityOverrides); this.validators = new ArrayList<>(validator.validators); - loadMetadataSuppressions(); - loadMetadataSeverityOverrides(); + // Suppressing and elevating events is handled by composing a given decorator with a + // ModelBasedEventDecorator. + ModelBasedEventDecorator modelBasedEventDecorator = new ModelBasedEventDecorator(); + modelBasedEventDecorator.severityOverrides(validator.severityOverrides); + ValidatedResult result = modelBasedEventDecorator.createDecorator(model); + this.validationEventDecorator = result.getResult() + .map(decorator -> ValidationEventDecorator.compose( + ListUtils.of(decorator, validator.validationEventDecorator))) + .orElse(validator.validationEventDecorator); + + // Events encountered while loading suppressions and overrides have been modified by everything the + // modelBasedEventDecorator knows about, but has not been modified by any custom decorator (if any). + for (ValidationEvent event : result.getValidationEvents()) { + if (validationEventDecorator.canDecorate(event)) { + event = validationEventDecorator.decorate(event); + } + events.add(event); + } - // Given events have already been emitted and decorated, but have not been suppressed/elevated. + // Now that the decorator is available, emit/decorate/suppress/collect explicitly provided events. for (ValidationEvent event : validator.events) { - events.add(modifyEventSeverity(event)); + pushEvent(event); } + // The decorator itself doesn't handle loading and applying validators, just modifying events. loadModelValidators(validator.validatorFactory); } - private void loadMetadataSeverityOverrides() { - model.getMetadataProperty(SEVERITY_OVERRIDES).ifPresent(value -> { - try { - List values = value.expectArrayNode().getElementsAs(ObjectNode.class); - for (ObjectNode rule : values) { - try { - severityOverrides.add(SeverityOverride.fromMetadata(rule)); - } catch (SourceException e) { - pushEvent(ValidationEvent.fromSourceException(e)); - } - } - } catch (SourceException e) { - pushEvent(ValidationEvent.fromSourceException(e)); - } - }); - } - - private void loadMetadataSuppressions() { - model.getMetadataProperty(SUPPRESSIONS).ifPresent(value -> { - try { - List values = value.expectArrayNode().getElementsAs(ObjectNode.class); - for (ObjectNode rule : values) { - try { - suppressions.add(Suppression.fromMetadata(rule)); - } catch (SourceException e) { - pushEvent(ValidationEvent.fromSourceException(e)); - } - } - } catch (SourceException e) { - pushEvent(ValidationEvent.fromSourceException(e)); - } - }); - } - private void loadModelValidators(ValidatorFactory validatorFactory) { // Load validators defined in metadata. ValidatedResult> loaded = ValidationLoader @@ -291,8 +265,9 @@ private void pushEvents(List source) { } private void pushEvent(ValidationEvent event) { - event = modifyEventSeverity(event); - event = validationEventDecorator.decorate(event); + if (validationEventDecorator.canDecorate(event)) { + event = validationEventDecorator.decorate(event); + } events.add(event); eventListener.accept(event); } @@ -310,8 +285,7 @@ private List validate() { List result = validators.parallelStream() .flatMap(validator -> validator.validate(model).stream()) .filter(this::filterPrelude) - .map(this::modifyEventSeverity) - .map(validationEventDecorator::decorate) + .map(e -> validationEventDecorator.canDecorate(e) ? validationEventDecorator.decorate(e) : e) // Emit events as they occur during validation. .peek(eventListener) .collect(Collectors.toList()); @@ -330,52 +304,5 @@ private boolean filterPrelude(ValidationEvent event) { .filter(Prelude::isPreludeShape) .isPresent(); } - - private ValidationEvent modifyEventSeverity(ValidationEvent event) { - // Use a suppress trait if present. - if (event.getShapeId().isPresent()) { - ShapeId target = event.getShapeId().get(); - Shape shape = model.getShape(target).orElse(null); - if (shape != null) { - if (shape.hasTrait(SuppressTrait.class)) { - Suppression suppression = Suppression.fromSuppressTrait(shape); - if (suppression.test(event)) { - return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null)); - } - } - } - } - - // Check metadata and manual suppressions. - for (Suppression suppression : suppressions) { - if (suppression.test(event)) { - return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null)); - } - } - - Severity appliedSeverity = event.getSeverity(); - for (SeverityOverride override : severityOverrides) { - Severity overrideResult = override.apply(event); - if (overrideResult.ordinal() > appliedSeverity.ordinal()) { - appliedSeverity = overrideResult; - } - } - - return changeSeverity(event, appliedSeverity, null); - } - - private static ValidationEvent changeSeverity(ValidationEvent event, Severity severity, String reason) { - if (event.getSeverity() == severity) { - return event; - } else { - // The event was suppressed so change the severity and reason. - ValidationEvent.Builder builder = event.toBuilder(); - builder.severity(severity); - if (reason != null) { - builder.suppressionReason(reason); - } - return builder.build(); - } - } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java index 8cfba31d16a..7090ea17f03 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java @@ -24,6 +24,20 @@ * relevant only to the context where Smithy is being used. */ public interface ValidationEventDecorator { + + /** A decorator that does nothing. */ + ValidationEventDecorator IDENTITY = new ValidationEventDecorator() { + @Override + public boolean canDecorate(ValidationEvent ev) { + return false; + } + + @Override + public ValidationEvent decorate(ValidationEvent ev) { + return ev; + } + }; + /** * Returns true if this decorator knows how to decorate this event, usually by looking at the event id. * @@ -48,17 +62,7 @@ public interface ValidationEventDecorator { */ static ValidationEventDecorator compose(List decorators) { if (decorators.isEmpty()) { - return new ValidationEventDecorator() { - @Override - public boolean canDecorate(ValidationEvent ev) { - return false; - } - - @Override - public ValidationEvent decorate(ValidationEvent ev) { - return ev; - } - }; + return IDENTITY; } else { return new ValidationEventDecorator() { @Override diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java new file mode 100644 index 00000000000..a90ce944c28 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java @@ -0,0 +1,199 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.validation.suppressions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.SuppressTrait; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidatedResult; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.ValidationEventDecorator; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Creates a {@link ValidationEventDecorator} that applies custom suppressions, custom severity overrides, + * suppressions parsed from model metadata, and severity overrides parsed from model metadata. + */ +@SmithyUnstableApi +public final class ModelBasedEventDecorator { + + private static final String SUPPRESSIONS = "suppressions"; + private static final String SEVERITY_OVERRIDES = "severityOverrides"; + + private final List suppressions = new ArrayList<>(); + private final List severityOverrides = new ArrayList<>(); + + /** + * Sets custom suppressions not found in the model. + * + * @param suppressions Suppressions to set. + * @return Returns the ModelBasedEventDecorator. + */ + public ModelBasedEventDecorator suppressions(Collection suppressions) { + this.suppressions.clear(); + this.suppressions.addAll(suppressions); + return this; + } + + /** + * Sets custom severity overrides not found in the model. + * + * @param severityOverrides Severity overrides to set. + * @return Returns the ModelBasedEventDecorator. + */ + public ModelBasedEventDecorator severityOverrides(Collection severityOverrides) { + this.severityOverrides.clear(); + this.severityOverrides.addAll(severityOverrides); + return this; + } + + /** + * Creates a ValidationEventDecorator for the given Model. + * + *

Validators, suppressions, and severity overrides found in the model are used each time + * {@link ValidationEventDecorator#decorate(ValidationEvent)} is called. The + * {@link ValidationEventDecorator#canDecorate(ValidationEvent)} always returns true. + * + * @param model Model to load validation events from. + * @return Returns a decorator that can be used to modify the severity and suppression reason of each given event. + */ + public ValidatedResult createDecorator(Model model) { + // Create dedicated arrays to separate the state of the created decorator from the builder. + List events = new ArrayList<>(); + List loadedSuppressions = new ArrayList<>(suppressions); + loadMetadataSuppressions(model, loadedSuppressions, events); + List loadedSeverityOverrides = new ArrayList<>(severityOverrides); + loadMetadataSeverityOverrides(model, loadedSeverityOverrides, events); + + // Modify severities and overrides of each encountered event. + for (int i = 0; i < events.size(); i++) { + events.set(i, modifyEventSeverity(model, events.get(i), loadedSuppressions, loadedSeverityOverrides)); + } + + return new ValidatedResult<>(new ValidationEventDecorator() { + @Override + public boolean canDecorate(ValidationEvent ev) { + return true; + } + + @Override + public ValidationEvent decorate(ValidationEvent ev) { + return modifyEventSeverity(model, ev, loadedSuppressions, loadedSeverityOverrides); + } + }, events); + } + + private static void loadMetadataSeverityOverrides( + Model model, + List severityOverrides, + List events + ) { + model.getMetadataProperty(SEVERITY_OVERRIDES).ifPresent(value -> { + try { + List values = value.expectArrayNode().getElementsAs(ObjectNode.class); + for (ObjectNode rule : values) { + try { + severityOverrides.add(SeverityOverride.fromMetadata(rule)); + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + } + } + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + } + }); + } + + private static void loadMetadataSuppressions( + Model model, + List suppressions, + List events + ) { + model.getMetadataProperty(SUPPRESSIONS).ifPresent(value -> { + try { + List values = value.expectArrayNode().getElementsAs(ObjectNode.class); + for (ObjectNode rule : values) { + try { + suppressions.add(Suppression.fromMetadata(rule)); + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + } + } + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + } + }); + } + + private static ValidationEvent modifyEventSeverity( + Model model, + ValidationEvent event, + List suppressions, + List severityOverrides + ) { + // Use a suppress trait if present. + if (event.getShapeId().isPresent()) { + ShapeId target = event.getShapeId().get(); + Shape shape = model.getShape(target).orElse(null); + if (shape != null) { + if (shape.hasTrait(SuppressTrait.class)) { + Suppression suppression = Suppression.fromSuppressTrait(shape); + if (suppression.test(event)) { + return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null)); + } + } + } + } + + // Check metadata and manual suppressions. + for (Suppression suppression : suppressions) { + if (suppression.test(event)) { + return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null)); + } + } + + Severity appliedSeverity = event.getSeverity(); + for (SeverityOverride override : severityOverrides) { + Severity overrideResult = override.apply(event); + if (overrideResult.ordinal() > appliedSeverity.ordinal()) { + appliedSeverity = overrideResult; + } + } + + return changeSeverity(event, appliedSeverity, null); + } + + private static ValidationEvent changeSeverity(ValidationEvent event, Severity severity, String reason) { + if (event.getSeverity() == severity) { + return event; + } else { + // The event was suppressed so change the severity and reason. + ValidationEvent.Builder builder = event.toBuilder(); + builder.severity(severity); + if (reason != null) { + builder.suppressionReason(reason); + } + return builder.build(); + } + } +} diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java new file mode 100644 index 00000000000..21243e69c88 --- /dev/null +++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java @@ -0,0 +1,95 @@ +package software.amazon.smithy.model.validation.suppressions; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidatedResult; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.ValidationEventDecorator; +import software.amazon.smithy.model.validation.Validator; +import software.amazon.smithy.model.validation.ValidatorFactory; + +public class ModelBasedEventDecoratorTest { + @Test + public void erroneousSuppressionsEmitEvents() { + ValidatedResult result = Model.assembler() + .addImport(getClass().getResource("bad-suppressions.smithy")) + // Ensure that events found while loading the model are decorated too. + .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build())) + .assemble(); + + assertThat(result.getValidationEvents(Severity.ERROR), not(empty())); + assertThat(result.getValidationEvents(Severity.ERROR).get(0).getMessage(), containsString("member `id`")); + assertThat(result.getValidationEvents(Severity.ERROR).get(0).getHint(), equalTo(Optional.of("hi"))); + } + + @Test + public void erroneousOverridesEmitEvents() { + ValidatedResult result = Model.assembler() + .addImport(getClass().getResource("bad-severityOverrides.smithy")) + // Ensure that events found while loading the model are decorated too. + .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build())) + .assemble(); + + assertThat(result.getValidationEvents(Severity.ERROR), not(empty())); + assertThat(result.getValidationEvents(Severity.ERROR).get(0).getMessage(), containsString("member `id`")); + assertThat(result.getValidationEvents(Severity.ERROR).get(0).getHint(), equalTo(Optional.of("hi"))); + } + + @Test + public void loadsSuppressionsAndOverrides() { + ValidatedResult result = Model.assembler() + .addImport(Model.class.getResource( + "errorfiles/validators/severityOverrides/suppressions-take-precedence.smithy")) + .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build())) + .assemble(); + + assertThat(result.getValidationEvents(Severity.ERROR), empty()); + assertThat(result.getValidationEvents(), not(empty())); + + // Every event should have the applied hint. + for (ValidationEvent event : result.getValidationEvents()) { + assertThat(event.getHint(), equalTo(Optional.of("hi"))); + } + } + + private static ValidatorFactory testFactory(Function d) { + return new ValidatorFactory() { + @Override + public List loadBuiltinValidators() { + return Collections.emptyList(); + } + + @Override + public Optional createValidator(String name, ObjectNode configuration) { + return Optional.empty(); + } + + @Override + public List loadDecorators() { + return Collections.singletonList(new ValidationEventDecorator() { + @Override + public boolean canDecorate(ValidationEvent ev) { + return true; + } + + @Override + public ValidationEvent decorate(ValidationEvent ev) { + return d.apply(ev); + } + }); + } + }; + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy new file mode 100644 index 00000000000..ba198649de5 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +metadata severityOverrides = [ + { + // missing id + } +] diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy new file mode 100644 index 00000000000..a942f4dabb2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +metadata suppressions = [ + { + // missing id + } +]