Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ private ProjectionResult applyProjection(
"The model could not be merged with the following imports: [%s]",
projection.getImports()));
return ProjectionResult.builder()
// Create an empty model so that ProjectionResult can be created when
// the Model can't be assembled.
.model(Model.builder().build())
.projectionName(projectionName)
.events(baseModel.getValidationEvents())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
Expand Down Expand Up @@ -46,12 +47,18 @@
import software.amazon.smithy.build.model.SmithyBuildConfig;
import software.amazon.smithy.build.model.TransformConfig;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.DocumentationTrait;
import software.amazon.smithy.model.traits.SensitiveTrait;
import software.amazon.smithy.model.traits.TagsTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.TraitFactory;
import software.amazon.smithy.model.validation.Severity;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.utils.IoUtils;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.MapUtils;
Expand Down Expand Up @@ -590,4 +597,63 @@ public void execute(PluginContext context) {
assertThat(e.getMessage(), containsString("(exampleProjection): java.lang.RuntimeException: Hi"));
assertThat(e.getSuppressed(), equalTo(new Throwable[]{canned}));
}

/*
This test causes SmithyBuild to fail in a way that it's unable to create Model as part of the
projection while using imports. This trigger a specific code path that needs to be able to create
a ProjectionResult with no model, so an empty model is provided. To achieve this, a test trait is
used with a custom trait factory that always throws when toNode is called on the trait (this is
something that's used for lots of stuff like trait hash code and equality). Smithy's model
assembler will catch the thrown SourceException, turn it into a validation event, and then bail
on trying to finish to build the model, triggering the code path under test here.
*/
@Test
public void projectionsThatCannotCreateModelsFailGracefully() throws URISyntaxException {
Path config = Paths.get(getClass().getResource("test-bad-trait-serializer-config.json").toURI());
Model model = Model.assembler()
.addImport(getClass().getResource("test-bad-trait-serializer.smithy"))
.assemble()
.unwrap();
ShapeId badId = ShapeId.from("smithy.test#badTrait");
TraitFactory baseFactory = TraitFactory.createServiceFactory();
SmithyBuild builder = new SmithyBuild()
.config(config)
.model(model)
.fileManifestFactory(MockManifest::new)
.modelAssemblerSupplier(() -> {
// Hook in a TraitFactory that knows about our custom, failing trait.
return Model.assembler()
.traitFactory((id, target, node) -> {
if (id.equals(badId)) {
return Optional.of(new BadCustomTrait());
} else {
return baseFactory.createTrait(id, target, node);
}
});
});

SmithyBuildResult result = builder.build();

// The project must have failed.
assertTrue(result.anyBroken());

// Now validate that we got the failure to serialize the model.
List<ValidationEvent> events = result.getProjectionResults().get(0).getEvents();
assertThat(events, not(empty()));
assertThat(events.get(0).getSeverity(), is(Severity.ERROR));
assertThat(events.get(0).getMessage(), containsString("Unable to serialize trait"));
}

// A test trait that always throws when toNode is called.
private static final class BadCustomTrait implements Trait {
@Override
public ShapeId toShapeId() {
return ShapeId.from("smithy.test#badTrait");
}

@Override
public Node toNode() {
throw new SourceException("Unable to serialize trait!", SourceLocation.none());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"version": "1.0",
"projections": {
"foo": {
"imports": ["test-bad-trait-serializer-import.smithy"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
$version: "1.0"

namespace smithy.test

@trait
string badTrait

@badTrait
string shape
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
$version: "1.0"

namespace smithy.test
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,16 @@ public interface SmithyBuilder<T> {
*/
static <T> T requiredState(String method, T value) {
if (value == null) {
throw new IllegalStateException(method + " was not set on the builder");
StringBuilder message = new StringBuilder(method).append(" was not set on the builder");

// Include the builder class that could not be built.
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
if (elements.length >= 2) {
String builder = elements[2].getClassName();
message.append(" (builder class is probably ").append(builder).append(')');
}

throw new IllegalStateException(message.toString());
}
return value;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2022 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.utils;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class SmithyBuilderTest {
@Test
public void includesCallingClassName() {
IllegalStateException e = Assertions.assertThrows(IllegalStateException.class, () -> new Foo().test());

assertThat(e.getMessage(), equalTo("foo was not set on the builder (builder class is probably "
+ "software.amazon.smithy.utils.SmithyBuilderTest$Foo)"));
}

private static final class Foo {
public void test() {
SmithyBuilder.requiredState("foo", null);
}
}
}