Skip to content

Commit 3224e71

Browse files
committed
Improve EndpointTests built-in validation
This commit adds new validation for built-in parameters when used with the `endpointTests` trait. 1. Validates that built-in values with defaults are specified to the correct value if they are mismatched in `operationInput` cases. 2. Validates that built-in values without defaults are specified in `operationInput` cases. Also fixes skipping DANGER/ERROR in endpoint test validation and adds a convenience method to the `Parameter` class.
1 parent 86ffc6a commit 3224e71

File tree

12 files changed

+394
-71
lines changed

12 files changed

+394
-71
lines changed

smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/parameters/Parameter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ public Identifier getName() {
118118
return name;
119119
}
120120

121+
/**
122+
* Gets the parameter name.
123+
*
124+
* @return returns the parameter name as a {@link String}.
125+
*/
126+
public String getNameString() {
127+
return name.toString();
128+
}
129+
121130
/**
122131
* Gets the parameter in template form.
123132
*

smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/EndpointTestsTraitValidator.java

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
import java.util.Map;
1111
import software.amazon.smithy.model.Model;
1212
import software.amazon.smithy.model.knowledge.TopDownIndex;
13+
import software.amazon.smithy.model.node.Node;
1314
import software.amazon.smithy.model.shapes.OperationShape;
1415
import software.amazon.smithy.model.shapes.ServiceShape;
1516
import software.amazon.smithy.model.shapes.StructureShape;
1617
import software.amazon.smithy.model.validation.AbstractValidator;
1718
import software.amazon.smithy.model.validation.NodeValidationVisitor;
1819
import software.amazon.smithy.model.validation.Severity;
1920
import software.amazon.smithy.model.validation.ValidationEvent;
21+
import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
22+
import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter;
2023
import software.amazon.smithy.utils.SmithyUnstableApi;
2124

2225
/**
@@ -36,7 +39,28 @@ public List<ValidationEvent> validate(Model model) {
3639
operationNameMap.put(operationShape.getId().getName(), operationShape);
3740
}
3841

42+
// Precompute the built-ins and their default states, as this will
43+
// be used frequently in downstream validation.
44+
List<Parameter> builtInParamsWithDefaults = new ArrayList<>();
45+
List<Parameter> builtInParamsWithoutDefaults = new ArrayList<>();
46+
EndpointRuleSet ruleSet = serviceShape.expectTrait(EndpointRuleSetTrait.class).getEndpointRuleSet();
47+
for (Parameter parameter : ruleSet.getParameters()) {
48+
if (parameter.isBuiltIn()) {
49+
if (parameter.getDefault().isPresent()) {
50+
builtInParamsWithDefaults.add(parameter);
51+
} else {
52+
builtInParamsWithoutDefaults.add(parameter);
53+
}
54+
}
55+
}
56+
3957
for (EndpointTestCase testCase : serviceShape.expectTrait(EndpointTestsTrait.class).getTestCases()) {
58+
// If values for built-in parameters don't match the default, they MUST
59+
// be specified in the operation inputs. Precompute the ones that don't match
60+
// and capture their value.
61+
Map<Parameter, Node> mismatchedBuiltInParams =
62+
getMismatchedBuiltInParams(builtInParamsWithDefaults, testCase);
63+
4064
for (EndpointTestOperationInput testOperationInput : testCase.getOperationInputs()) {
4165
String operationName = testOperationInput.getOperationName();
4266

@@ -47,42 +71,102 @@ public List<ValidationEvent> validate(Model model) {
4771
String.format("Test case operation `%s` does not exist in service `%s`",
4872
operationName,
4973
serviceShape.getId())));
74+
continue;
5075
}
5176

5277
// Still emit events if the operation exists, but was just not bound.
53-
if (operationNameMap.containsKey(operationName)) {
54-
StructureShape inputShape = model.expectShape(
55-
operationNameMap.get(operationName).getInputShape(),
56-
StructureShape.class);
57-
58-
List<ValidationEvent> operationInputEvents = validateOperationInput(model,
59-
serviceShape,
60-
inputShape,
61-
testOperationInput);
62-
63-
// Error test cases may use invalid inputs as the mechanism to trigger their error,
64-
// so lower the severity before emitting. All other events here should be raised to
65-
// DANGER level as well.
66-
for (ValidationEvent event : operationInputEvents) {
67-
if (event.getSeverity() == Severity.WARNING
68-
|| event.getSeverity() == Severity.NOTE
69-
|| testCase.getExpect().getError().isPresent()) {
70-
events.add(event.toBuilder().severity(Severity.DANGER).build());
71-
}
72-
}
73-
}
78+
validateBuiltInValuesAreValid(serviceShape,
79+
mismatchedBuiltInParams,
80+
testOperationInput,
81+
events);
82+
validateNoDefaultBuiltInParamsHaveValues(serviceShape,
83+
builtInParamsWithoutDefaults,
84+
testCase,
85+
testOperationInput,
86+
events);
87+
88+
StructureShape inputShape = model.expectShape(
89+
operationNameMap.get(operationName).getInputShape(),
90+
StructureShape.class);
91+
validateOperationInput(model, serviceShape, inputShape, testCase, testOperationInput, events);
7492
}
7593
}
7694
}
7795

7896
return events;
7997
}
8098

81-
private List<ValidationEvent> validateOperationInput(
99+
private Map<Parameter, Node> getMismatchedBuiltInParams(
100+
List<Parameter> builtInParamsWithDefaults,
101+
EndpointTestCase testCase
102+
) {
103+
Map<Parameter, Node> mismatchedBuiltInParams = new HashMap<>();
104+
for (Parameter parameter : builtInParamsWithDefaults) {
105+
String parameterName = parameter.getName().toString();
106+
Node defaultValue = parameter.getDefault().get().toNode();
107+
108+
// Consider a parameter mismatched if the built-in's default and its
109+
// value in this test case aren't the same.
110+
if (testCase.getParams().containsMember(parameterName)
111+
&& !testCase.getParams().expectMember(parameterName).equals(defaultValue)) {
112+
mismatchedBuiltInParams.put(parameter, testCase.getParams().expectMember(parameterName));
113+
}
114+
}
115+
return mismatchedBuiltInParams;
116+
}
117+
118+
private void validateBuiltInValuesAreValid(
119+
ServiceShape serviceShape,
120+
Map<Parameter, Node> mismatchedBuiltInParams,
121+
EndpointTestOperationInput testOperationInput,
122+
List<ValidationEvent> events
123+
) {
124+
for (Map.Entry<Parameter, Node> mismatchedBuiltInParam : mismatchedBuiltInParams.entrySet()) {
125+
String builtInName = mismatchedBuiltInParam.getKey().getBuiltIn().get();
126+
// Emit if either the built-in with a mismatched value isn't
127+
// specified or the value set for it doesn't match.
128+
if (!testOperationInput.getBuiltInParams().containsMember(builtInName)
129+
|| !testOperationInput.getBuiltInParams()
130+
.expectMember(builtInName)
131+
.equals(mismatchedBuiltInParam.getValue())) {
132+
events.add(error(serviceShape,
133+
testOperationInput,
134+
String.format("Test case does not supply the `%s` value for the `%s` parameter's "
135+
+ "`%s` built-in.",
136+
Node.printJson(mismatchedBuiltInParam.getValue()),
137+
mismatchedBuiltInParam.getKey().getNameString(),
138+
mismatchedBuiltInParam.getKey().getBuiltIn().get())));
139+
}
140+
}
141+
}
142+
143+
private void validateNoDefaultBuiltInParamsHaveValues(
144+
ServiceShape serviceShape,
145+
List<Parameter> builtInParamsWithoutDefaults,
146+
EndpointTestCase testCase,
147+
EndpointTestOperationInput testOperationInput,
148+
List<ValidationEvent> events
149+
) {
150+
for (Parameter parameter : builtInParamsWithoutDefaults) {
151+
if (testCase.getParams().containsMember(parameter.getNameString())
152+
&& !testOperationInput.getBuiltInParams().containsMember(parameter.getBuiltIn().get())) {
153+
events.add(error(serviceShape,
154+
testOperationInput,
155+
String.format("Operation input does not supply a value for the `%s` built-in parameter "
156+
+ "and the `%s` parameter does not set a default.",
157+
parameter.getBuiltIn().get(),
158+
parameter.getName())));
159+
}
160+
}
161+
}
162+
163+
private void validateOperationInput(
82164
Model model,
83165
ServiceShape serviceShape,
84166
StructureShape inputShape,
85-
EndpointTestOperationInput testOperationInput
167+
EndpointTestCase testCase,
168+
EndpointTestOperationInput testOperationInput,
169+
List<ValidationEvent> events
86170
) {
87171
NodeValidationVisitor validator = NodeValidationVisitor.builder()
88172
.model(model)
@@ -92,6 +176,18 @@ private List<ValidationEvent> validateOperationInput(
92176
.startingContext("The operationInput value for an endpoint test "
93177
+ "does not match the operation's input shape")
94178
.build();
95-
return inputShape.accept(validator);
179+
180+
// Error test cases may use invalid inputs as the mechanism to trigger their error,
181+
// so lower the severity before emitting. All other events here should be raised to
182+
// DANGER level as well.
183+
for (ValidationEvent event : inputShape.accept(validator)) {
184+
if (event.getSeverity() == Severity.WARNING
185+
|| event.getSeverity() == Severity.NOTE
186+
|| testCase.getExpect().getError().isPresent()) {
187+
events.add(event.toBuilder().severity(Severity.DANGER).build());
188+
} else {
189+
events.add(event);
190+
}
191+
}
96192
}
97193
}

smithy-rules-engine/src/test/java/software/amazon/smithy/rulesengine/traits/EndpointTestsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void loadsFromTheModel() {
3131
EndpointTestsTrait ruleSetTrait = serviceShape.getTrait(EndpointTestsTrait.class).get();
3232
List<EndpointTestCase> testCases = ruleSetTrait.getTestCases();
3333

34-
assertThat(2, equalTo(testCases.size()));
34+
assertThat(3, equalTo(testCases.size()));
3535
assertThat(EndpointTestCase.builder()
3636
.documentation("a documentation string")
3737
.params(ObjectNode.builder()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[WARNING] smithy.example#ExampleService: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
2+
[WARNING] smithy.example#ExampleService: This shape applies a trait that is unstable: smithy.rules#endpointTests | UnstableTrait
3+
[ERROR] smithy.example#ExampleService: Operation input does not supply a value for the `SDK::Endpoint` built-in parameter and the `endpoint` parameter does not set a default. | EndpointTestsTrait
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
$version: "2.0"
2+
3+
namespace smithy.example
4+
5+
use smithy.rules#endpointRuleSet
6+
use smithy.rules#endpointTests
7+
8+
@endpointRuleSet({
9+
version: "1.0"
10+
parameters: {
11+
endpoint: {
12+
type: "string"
13+
builtIn: "SDK::Endpoint"
14+
documentation: "docs"
15+
}
16+
}
17+
rules: [
18+
{
19+
documentation: "Passthrough"
20+
conditions: []
21+
endpoint: {
22+
url: "https://example.com"
23+
}
24+
type: "endpoint"
25+
}
26+
]
27+
})
28+
@endpointTests({
29+
version: "1.0"
30+
testCases: [
31+
{
32+
params: {
33+
endpoint: "https://example.com"
34+
}
35+
operationInputs: [{
36+
operationName: "GetThing"
37+
}]
38+
expect: {
39+
endpoint: {
40+
url: "https://example.com"
41+
}
42+
}
43+
}
44+
]
45+
})
46+
@suppress(["RuleSetParameter.TestCase.Unused"])
47+
service ExampleService {
48+
version: "2020-07-02"
49+
operations: [GetThing]
50+
}
51+
52+
operation GetThing {
53+
input := {
54+
fizz: String
55+
}
56+
}
57+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[WARNING] smithy.example#ExampleService: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
2+
[WARNING] smithy.example#ExampleService: This shape applies a trait that is unstable: smithy.rules#endpointTests | UnstableTrait
3+
[ERROR] smithy.example#ExampleService: Test case does not supply the `"https://another.example.com"` value for the `endpoint` parameter's `SDK::Endpoint` built-in. | EndpointTestsTrait
4+
[ERROR] smithy.example#ExampleService: Test case does not supply the `"https://some.example.com"` value for the `endpoint` parameter's `SDK::Endpoint` built-in. | EndpointTestsTrait
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
$version: "2.0"
2+
3+
namespace smithy.example
4+
5+
use smithy.rules#endpointRuleSet
6+
use smithy.rules#endpointTests
7+
8+
@endpointRuleSet({
9+
version: "1.0"
10+
parameters: {
11+
endpoint: {
12+
type: "string"
13+
builtIn: "SDK::Endpoint"
14+
documentation: "docs"
15+
default: "https://example.com"
16+
required: true
17+
}
18+
}
19+
rules: [
20+
{
21+
documentation: "Passthrough"
22+
conditions: []
23+
endpoint: {
24+
url: "https://example.com"
25+
}
26+
type: "endpoint"
27+
}
28+
]
29+
})
30+
@endpointTests({
31+
version: "1.0"
32+
testCases: [
33+
{
34+
params: {
35+
endpoint: "https://another.example.com"
36+
}
37+
operationInputs: [{
38+
operationName: "GetThing"
39+
builtInParams: {
40+
"SDK::Endpoint": "https://custom.example.com"
41+
}
42+
}]
43+
expect: {
44+
endpoint: {
45+
url: "https://example.com"
46+
}
47+
}
48+
}
49+
{
50+
params: {
51+
endpoint: "https://some.example.com"
52+
}
53+
operationInputs: [{
54+
operationName: "GetThing"
55+
builtInParams: {}
56+
}]
57+
expect: {
58+
endpoint: {
59+
url: "https://example.com"
60+
}
61+
}
62+
}
63+
{
64+
params: {
65+
endpoint: "https://valid.example.com"
66+
}
67+
operationInputs: [{
68+
operationName: "GetThing"
69+
builtInParams: {
70+
"SDK::Endpoint": "https://valid.example.com"
71+
}
72+
}]
73+
expect: {
74+
endpoint: {
75+
url: "https://example.com"
76+
}
77+
}
78+
}
79+
]
80+
})
81+
@suppress(["RuleSetParameter.TestCase.Unused"])
82+
service ExampleService {
83+
version: "2020-07-02"
84+
operations: [GetThing]
85+
}
86+
87+
operation GetThing {
88+
input := {
89+
fizz: String
90+
}
91+
}
92+
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
[WARNING] smithy.example#InvalidService: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
12
[WARNING] smithy.example#InvalidService: This shape applies a trait that is unstable: smithy.rules#endpointTests | UnstableTrait
23
[DANGER] smithy.example#InvalidService: The operationInput value for an endpoint test does not match the operation's input shape: Invalid structure member `fizz` found for `smithy.example#GetThingInput` | EndpointTestsTrait.smithy.example#GetThingInput.fizz

0 commit comments

Comments
 (0)