1 /*
2  * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 
16 package software.amazon.smithy.go.codegen.integration;
17 
18 import java.util.ArrayList;
19 import java.util.Comparator;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Objects;
23 import java.util.Set;
24 import java.util.TreeSet;
25 import java.util.function.Consumer;
26 import java.util.logging.Logger;
27 import software.amazon.smithy.codegen.core.CodegenException;
28 import software.amazon.smithy.codegen.core.Symbol;
29 import software.amazon.smithy.codegen.core.SymbolProvider;
30 import software.amazon.smithy.go.codegen.GoWriter;
31 import software.amazon.smithy.go.codegen.ShapeValueGenerator;
32 import software.amazon.smithy.go.codegen.SmithyGoDependency;
33 import software.amazon.smithy.model.Model;
34 import software.amazon.smithy.model.node.ObjectNode;
35 import software.amazon.smithy.model.shapes.MemberShape;
36 import software.amazon.smithy.model.shapes.OperationShape;
37 import software.amazon.smithy.model.shapes.ServiceShape;
38 import software.amazon.smithy.model.shapes.ShapeId;
39 import software.amazon.smithy.model.shapes.StructureShape;
40 import software.amazon.smithy.model.traits.IdempotencyTokenTrait;
41 import software.amazon.smithy.protocoltests.traits.HttpMessageTestCase;
42 import software.amazon.smithy.utils.SmithyBuilder;
43 
44 /**
45  * Abstract base implementation for protocol test generators to extend in order to generate HttpMessageTestCase
46  * specific protocol tests.
47  *
48  * @param <T> Specific HttpMessageTestCase the protocol test generator is for.
49  */
50 public abstract class HttpProtocolUnitTestGenerator<T extends HttpMessageTestCase> {
51     private static final Logger LOGGER = Logger.getLogger(HttpProtocolUnitTestGenerator.class.getName());
52 
53     protected final Model model;
54     protected final SymbolProvider symbolProvider;
55     protected final List<T> testCases;
56     protected final ServiceShape service;
57     protected final OperationShape operation;
58     protected final Symbol opSymbol;
59     protected final StructureShape inputShape;
60     protected final Symbol inputSymbol;
61     protected final StructureShape outputShape;
62     protected final Symbol outputSymbol;
63     protected final String protocolName;
64     protected final Set<ConfigValue> clientConfigValues = new TreeSet<>();
65     protected final Set<SkipTest> skipTests = new TreeSet<>();
66     protected final ShapeValueGenerator.Config shapeValueGeneratorConfig;
67 
68     /**
69      * Initializes the abstract protocol tests generator.
70      *
71      * @param builder the builder initializing the generator.
72      */
HttpProtocolUnitTestGenerator(Builder<T> builder)73     protected HttpProtocolUnitTestGenerator(Builder<T> builder) {
74         this.model = SmithyBuilder.requiredState("model", builder.model);
75         this.symbolProvider = SmithyBuilder.requiredState("symbolProvider", builder.symbolProvider);
76         this.protocolName = SmithyBuilder.requiredState("protocolName", builder.protocolName);
77         this.service = SmithyBuilder.requiredState("service", builder.service);
78         this.operation = SmithyBuilder.requiredState("operation", builder.operation);
79         this.testCases = SmithyBuilder.requiredState("testCases", builder.testCases);
80         this.clientConfigValues.addAll(builder.clientConfigValues);
81         this.skipTests.addAll(builder.skipTests);
82         this.shapeValueGeneratorConfig = SmithyBuilder.requiredState("config", builder.shapeValueGeneratorConfig);
83 
84         opSymbol = symbolProvider.toSymbol(operation);
85 
86         inputShape = model.expectShape(operation.getInput()
87                 .orElseThrow(() -> new CodegenException("missing input shape for operation: " + operation.getId())),
88                 StructureShape.class);
89         inputSymbol = symbolProvider.toSymbol(inputShape);
90 
91         outputShape = model.expectShape(operation.getOutput()
92                 .orElseThrow(() -> new CodegenException("missing output shape for operation: " + operation.getId())),
93                 StructureShape.class);
94         outputSymbol = symbolProvider.toSymbol(outputShape);
95     }
96 
97     /**
98      * Provides the unit test function's format string.
99      *
100      * @return returns format string paired with unitTestFuncNameArgs
101      */
unitTestFuncNameFormat()102     abstract String unitTestFuncNameFormat();
103 
104     /**
105      * Provides the unit test function name's format string arguments.
106      *
107      * @return returns a list of arguments used to format the unitTestFuncNameFormat returned format string.
108      */
unitTestFuncNameArgs()109     abstract Object[] unitTestFuncNameArgs();
110 
111 
112     /**
113      * Hook to provide custom generated code within a test function before test cases are defined.
114      *
115      * @param writer writer to write generated code with.
116      */
generateTestSetup(GoWriter writer)117     protected void generateTestSetup(GoWriter writer) {
118         // Pass
119     }
120 
121     /**
122      * Hook to generate the parameter declarations as struct parameters into the test case's struct definition.
123      * Must generate all test case parameters before returning.
124      *
125      * @param writer writer to write generated code with.
126      */
generateTestCaseParams(GoWriter writer)127     abstract void generateTestCaseParams(GoWriter writer);
128 
129     /**
130      * Hook to generate all the test case parameters as struct member values for a single test case.
131      * Must generate all test case parameter values before returning.
132      *
133      * @param writer   writer to write generated code with.
134      * @param testCase definition of a single test case.
135      */
generateTestCaseValues(GoWriter writer, T testCase)136     abstract void generateTestCaseValues(GoWriter writer, T testCase);
137 
138     /**
139      * Hook to optionally generate additional setup needed before the test body is created.
140      *
141      * @param writer writer to write generated code with.
142      */
generateTestBodySetup(GoWriter writer)143     protected void generateTestBodySetup(GoWriter writer) {
144         // pass
145     }
146 
147     /**
148      * Hook to generate the HTTP response body of the protocol test. If overriding and delegating to this method must
149      * the last usage of ResponseWriter.
150      *
151      * @param writer writer to write generated code with.
152      */
generateTestServerHandler(GoWriter writer)153     protected void generateTestServerHandler(GoWriter writer) {
154         writer.write("w.WriteHeader(200)");
155     }
156 
157     /**
158      * Hook to generate the HTTP test server that will receive requests and provide responses back to the requester.
159      *
160      * @param writer  writer to write generated code with.
161      * @param name    test server variable name
162      * @param handler lambda for writing handling of HTTP request
163      */
generateTestServer(GoWriter writer, String name, Consumer<GoWriter> handler)164     protected void generateTestServer(GoWriter writer, String name, Consumer<GoWriter> handler) {
165         writer.addUseImports(SmithyGoDependency.NET_HTTP);
166         writer.addUseImports(SmithyGoDependency.NET_HTTP_TEST);
167         writer.openBlock("$L := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){",
168                 "}))", name, () -> {
169                     handler.accept(writer);
170                 });
171         writer.write("defer $L.Close()", name);
172         writer.write("url := server.URL");
173     }
174 
175     /**
176      * Hook to generate the instance of the API client for the protocol test.
177      *
178      * @param writer     writer to write generated code with.
179      * @param clientName test client variable name
180      */
generateTestClient(GoWriter writer, String clientName)181     protected void generateTestClient(GoWriter writer, String clientName) {
182         writer.openBlock("$L := New(Options{", "})", clientName, () -> {
183             for (ConfigValue value : clientConfigValues) {
184                 writeStructField(writer, value.getName(), value.getValue());
185             }
186         });
187     }
188 
189     /**
190      * Hook to generate the client invoking the API operation of the test. Should not do any assertions.
191      *
192      * @param writer     writer to write generated code with.
193      * @param clientName name of the client variable.
194      */
generateTestInvokeClientOperation(GoWriter writer, String clientName)195     abstract void generateTestInvokeClientOperation(GoWriter writer, String clientName);
196 
197     /**
198      * Hook to generate the assertions for the operation's test cases. Will be in the same scope as the test body.
199      *
200      * @param writer writer to write generated code with.
201      */
generateTestAssertions(GoWriter writer)202     abstract void generateTestAssertions(GoWriter writer);
203 
204     /**
205      * Generates the test function for the operation using the provided writer.
206      *
207      * @param writer writer to write generated code with.
208      */
generateTestFunction(GoWriter writer)209     public void generateTestFunction(GoWriter writer) {
210         writer.addUseImports(SmithyGoDependency.TESTING);
211         writer.openBlock("func " + unitTestFuncNameFormat() + "(t *testing.T) {", "}", unitTestFuncNameArgs(),
212                 () -> {
213                     skipTests.forEach((skipTest) -> {
214                         if (skipTest.matches(service.getId(), operation.getId())) {
215                             writer.write("t.Skip(\"disabled test $L $L\")", service.getId(), operation.getId());
216                             writer.write("");
217                         }
218                     });
219                     generateTestSetup(writer);
220 
221                     writer.write("cases := map[string]struct {");
222                     generateTestCaseParams(writer);
223                     writer.openBlock("}{", "}", () -> {
224                         for (T testCase : testCases) {
225                             testCase.getDocumentation().ifPresent(writer::writeDocs);
226                             writer.openBlock("$S: {", "},", testCase.getId(), () -> {
227                                 generateTestCaseValues(writer, testCase);
228                             });
229                         }
230                     });
231 
232                     // And test case iteration/assertions
233                     writer.openBlock("for name, c := range cases {", "}", () -> {
234                         writer.openBlock("t.Run(name, func(t *testing.T) {", "})", () -> {
235                             skipTests.forEach((skipTest) -> {
236                                 for (T testCase : testCases) {
237                                     if (skipTest.matches(service.getId(), operation.getId(), testCase.getId())) {
238                                         writer.openBlock("if name == $S {", "}", testCase.getId(), () -> {
239                                             writer.write("t.Skip(\"disabled test $L $L\")", service.getId(),
240                                                     operation.getId());
241                                         });
242                                         writer.write("");
243                                     }
244                                 }
245                             });
246 
247                             generateTestBodySetup(writer);
248                             generateTestServer(writer, "server", this::generateTestServerHandler);
249                             generateTestClient(writer, "client");
250                             generateTestInvokeClientOperation(writer, "client");
251                             generateTestAssertions(writer);
252                         });
253                     });
254                 });
255     }
256 
257     /**
258      * Writes a single Go structure field key and value.
259      *
260      * @param writer writer to write generated code with.
261      * @param field  the field name of the struct member.
262      * @param value  the value of the struct member.
263      */
writeStructField(GoWriter writer, String field, Object value)264     protected void writeStructField(GoWriter writer, String field, Object value) {
265         writer.write("$L: $L,", field, value);
266     }
267 
268     /**
269      * Writes a  single Go structure field key and value. Provides inline formatting of the field value.
270      *
271      * @param writer      writer to write generated code with.
272      * @param field       the field name of the struct member.
273      * @param valueFormat the format string to use for the field value
274      * @param args        the format string arguments for the field value.
275      */
writeStructField(GoWriter writer, String field, String valueFormat, Object... args)276     protected void writeStructField(GoWriter writer, String field, String valueFormat, Object... args) {
277         writer.writeInline("$L: ", field);
278         writer.writeInline(valueFormat, args);
279         writer.write(",");
280     }
281 
282     /**
283      * Writes a single Go structure field key and value. Writes the field value inline from the shape and
284      * ObjectNode graph provided.
285      *
286      * @param writer writer to write generated code with.
287      * @param field  the field name of the struct member.
288      * @param shape  the shape the field member.
289      * @param params the node of values to fill the member with.
290      */
writeStructField(GoWriter writer, String field, StructureShape shape, ObjectNode params)291     protected void writeStructField(GoWriter writer, String field, StructureShape shape, ObjectNode params) {
292         writer.writeInline("$L: ", field);
293         writeShapeValueInline(writer, shape, params);
294         writer.write(",");
295     }
296 
297     /**
298      * Writes a single Go structure field key and value. Writes the field value inline from the shape and
299      * ObjectNode graph provided. Value writer is responsible for writing the proceeding comma after the value.
300      *
301      * @param writer writer to write generated code with.
302      * @param field  the field name of the struct member.
303      * @param value  inline value writer.
304      */
writeStructField(GoWriter writer, String field, Consumer<GoWriter> value)305     protected void writeStructField(GoWriter writer, String field, Consumer<GoWriter> value) {
306         writer.writeInline("$L: ", field);
307         value.accept(writer);
308     }
309 
310     /**
311      * Writes a Go structure field for a QueryItem value.
312      *
313      * @param writer writer to write generated code with.
314      * @param field  the name of the field.
315      * @param values list of values for the query.
316      */
writeQueryItemsStructField(GoWriter writer, String field, List<String> values)317     protected void writeQueryItemsStructField(GoWriter writer, String field, List<String> values) {
318         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
319         writer.openBlock("$L: []smithytesting.QueryItem {", "},", field, () -> {
320             writeQueryItemsValues(writer, values);
321         });
322     }
323 
324     /**
325      * Writes values of query items as slice members.
326      *
327      * @param writer writer to write generated code with.
328      * @param values list of values for the query.
329      */
writeQueryItemsValues(GoWriter writer, List<String> values)330     protected void writeQueryItemsValues(GoWriter writer, List<String> values) {
331         for (String item : values) {
332             String[] parts = item.split("=", 2);
333             String value = "";
334             if (parts.length > 1) {
335                 value = parts[1];
336             }
337             writer.write("{Key: $S, Value: $S},", parts[0], value);
338         }
339     }
340 
341     /**
342      * Writes utility to breakout RawQuery string into its components for testing.
343      *
344      * @param writer writer to write generated code with.
345      * @param source name of variable containing raw query string.
346      * @param target name of destination variable that will be created to hold QueryItems
347      */
writeQueryItemBreakout(GoWriter writer, String source, String target)348     protected void writeQueryItemBreakout(GoWriter writer, String source, String target) {
349         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
350         writer.write("$L := smithytesting.ParseRawQuery($L)", target, source);
351     }
352 
353     /**
354      * Writes a structure header member with values from a map.
355      *
356      * @param writer writer to write generated code with.
357      * @param field  name of the field.
358      * @param values map of header key and value pairs.
359      */
writeHeaderStructField(GoWriter writer, String field, Map<String, String> values)360     protected void writeHeaderStructField(GoWriter writer, String field, Map<String, String> values) {
361         if (values.size() == 0) {
362             return;
363         }
364         writer.openBlock("$L: http.Header{", "},", field, () -> {
365             writeHeaderValues(writer, values);
366         });
367     }
368 
369     /**
370      * Writes individual header key value field pairs.
371      *
372      * @param writer writer to write generated code with.
373      * @param values map of header key/value pairs.
374      */
writeHeaderValues(GoWriter writer, Map<String, String> values)375     protected void writeHeaderValues(GoWriter writer, Map<String, String> values) {
376         values.forEach((k, v) -> {
377             writer.write("$S: []string{$S},", k, v);
378         });
379     }
380 
381     /**
382      * Writes a string slice to a struct field.
383      *
384      * @param writer writer to write generated code with.
385      * @param field  the name of the field.
386      * @param values the list of field values.
387      */
writeStringSliceStructField(GoWriter writer, String field, List<String> values)388     protected void writeStringSliceStructField(GoWriter writer, String field, List<String> values) {
389         if (values.size() == 0) {
390             return;
391         }
392 
393         writer.openBlock("$L: []string{", "},", field, () -> {
394             writeStringSliceValues(writer, values);
395         });
396     }
397 
398     /**
399      * Writes a list of strings as go string slice members.
400      *
401      * @param writer writer to write generated code with.
402      * @param values the list of string values.
403      */
writeStringSliceValues(GoWriter writer, List<String> values)404     protected void writeStringSliceValues(GoWriter writer, List<String> values) {
405         for (String value : values) {
406             writer.write("$S,", value);
407         }
408     }
409 
410     /**
411      * Writes the assertion for comparing two scalar values.
412      *
413      * @param writer writer to write generated code with.
414      * @param expect variable name of the expected value.
415      * @param actual variable name of the actual value.
416      * @param tag    additional error message description.
417      */
writeAssertScalarEqual(GoWriter writer, String expect, String actual, String tag)418     protected void writeAssertScalarEqual(GoWriter writer, String expect, String actual, String tag) {
419         writer.openBlock("if e, a := $L, $L; e != a {", "}", expect, actual, () -> {
420             writer.write("t.Errorf(\"expect %v $L, got %v\", e, a)", tag);
421         });
422     }
423 
424     /**
425      * Writes the assertion for comparing two complex type values, e.g. structures.
426      *
427      * @param writer      writer to write generated code with.
428      * @param expect      the variable name of the expected value.
429      * @param actual      the variable name of the actual value.
430      * @param ignoreTypes list of type values that should be ignored by the compare.
431      */
writeAssertComplexEqual( GoWriter writer, String expect, String actual, String[] ignoreTypes )432     protected void writeAssertComplexEqual(
433             GoWriter writer, String expect, String actual, String[] ignoreTypes
434     ) {
435         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
436         writer.addUseImports(SmithyGoDependency.GO_CMP_OPTIONS);
437 
438         writer.writeInline("if err := smithytesting.CompareValues($L, $L, cmpopts.IgnoreUnexported(", expect, actual);
439 
440         for (String ignoreType : ignoreTypes) {
441             writer.write("$L,", ignoreType);
442         }
443 
444         writer.writeInline(")); err != nil {");
445         writer.write("   t.Errorf(\"expect $L value match:\\n%v\", err)", expect);
446         writer.write("}");
447     }
448 
449     /**
450      * Writes assertion that a variable's value must be nil.
451      *
452      * @param writer writer to write generated code with.
453      * @param field  the variable name of the value.
454      */
writeAssertNil(GoWriter writer, String field)455     protected void writeAssertNil(GoWriter writer, String field) {
456         writer.openBlock("if $L != nil {", "}", field, () -> {
457             writer.write("t.Fatalf(\"expect nil $L, got %v\", $L)", field, field);
458         });
459     }
460 
461     /**
462      * Writes the assertion that a variable must not be nil.
463      *
464      * @param writer writer to write generated code with.
465      * @param field  the variable name of the value.
466      */
writeAssertNotNil(GoWriter writer, String field)467     protected void writeAssertNotNil(GoWriter writer, String field) {
468         writer.openBlock("if $L == nil {", "}", field, () -> {
469             writer.write("t.Fatalf(\"expect not nil $L\")", field);
470         });
471     }
472 
473     /**
474      * Writes the assertion that query contains expected values.
475      *
476      * @param writer writer to write generated code with.
477      * @param expect variable name with the expected values.
478      * @param actual variable name with the actual values.
479      */
writeAssertHasQuery(GoWriter writer, String expect, String actual)480     void writeAssertHasQuery(GoWriter writer, String expect, String actual) {
481         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
482         writer.write("smithytesting.AssertHasQuery(t, $L, $L)", expect, actual);
483     }
484 
485     /**
486      * Writes the assertion that an query contains keys.
487      *
488      * @param writer writer to write generated code with.
489      * @param expect variable name with the expected values.
490      * @param actual variable name with the actual values.
491      */
writeAssertRequireQuery(GoWriter writer, String expect, String actual)492     protected void writeAssertRequireQuery(GoWriter writer, String expect, String actual) {
493         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
494         writer.write("smithytesting.AssertHasQueryKeys(t, $L, $L)", expect, actual);
495     }
496 
497     /**
498      * Writes the assertion that an query must not contain keys.
499      *
500      * @param writer writer to write generated code with.
501      * @param expect variable name with the expected values.
502      * @param actual variable name with the actual values.
503      */
writeAssertForbidQuery(GoWriter writer, String expect, String actual)504     protected void writeAssertForbidQuery(GoWriter writer, String expect, String actual) {
505         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
506         writer.write("smithytesting.AssertNotHaveQueryKeys(t, $L, $L)", expect, actual);
507     }
508 
509     /**
510      * Writes the assertion that headers contain expected values.
511      *
512      * @param writer writer to write generated code with.
513      * @param expect variable name with the expected values.
514      * @param actual variable name with the actual values.
515      */
writeAssertHasHeader(GoWriter writer, String expect, String actual)516     protected void writeAssertHasHeader(GoWriter writer, String expect, String actual) {
517         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
518         writer.write("smithytesting.AssertHasHeader(t, $L, $L)", expect, actual);
519     }
520 
521     /**
522      * Writes the assertion that the header contains keys.
523      *
524      * @param writer writer to write generated code with.
525      * @param expect variable name with the expected values.
526      * @param actual variable name with the actual values.
527      */
writeAssertRequireHeader(GoWriter writer, String expect, String actual)528     protected void writeAssertRequireHeader(GoWriter writer, String expect, String actual) {
529         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
530         writer.write("smithytesting.AssertHasHeaderKeys(t, $L, $L)", expect, actual);
531     }
532 
533     /**
534      * Writes the assertion that the header must not contain keys.
535      *
536      * @param writer writer to write generated code with.
537      * @param expect variable name with the expected values.
538      * @param actual variable name with the actual values.
539      */
writeAssertForbidHeader(GoWriter writer, String expect, String actual)540     protected void writeAssertForbidHeader(GoWriter writer, String expect, String actual) {
541         writer.addUseImports(SmithyGoDependency.SMITHY_TESTING);
542         writer.write("smithytesting.AssertNotHaveHeaderKeys(t, $L, $L)", expect, actual);
543     }
544 
545     /**
546      * Writes a shape type declaration value filled with values in the ObjectNode.
547      *
548      * @param writer writer to write generated code with.
549      * @param shape  shape of the value type to be created.
550      * @param params values to initialize shape type with.
551      */
writeShapeValueInline(GoWriter writer, StructureShape shape, ObjectNode params)552     protected void writeShapeValueInline(GoWriter writer, StructureShape shape, ObjectNode params) {
553         new ShapeValueGenerator(model, symbolProvider, shapeValueGeneratorConfig)
554                 .writePointableStructureShapeValueInline(writer, shape, params);
555     }
556 
557     /**
558      * Returns if the operation has an idempotency token input member.
559      *
560      * @return if the operation has an idempotency token input member.
561      */
hasIdempotencyTokenInputMember()562     private boolean hasIdempotencyTokenInputMember() {
563         for (MemberShape member : inputShape.members()) {
564             if (member.hasTrait(IdempotencyTokenTrait.class)) {
565                 return true;
566             }
567         }
568         return false;
569     }
570 
571     public abstract static class Builder<T extends HttpMessageTestCase> {
572         protected Model model;
573         protected SymbolProvider symbolProvider;
574         protected String protocolName = "";
575         protected ServiceShape service;
576         protected OperationShape operation;
577         protected List<T> testCases = new ArrayList<>();
578         protected Set<ConfigValue> clientConfigValues = new TreeSet<>();
579         protected Set<SkipTest> skipTests = new TreeSet<>();
580         protected ShapeValueGenerator.Config shapeValueGeneratorConfig = ShapeValueGenerator.Config.builder().build();
581 
model(Model model)582         public Builder<T> model(Model model) {
583             this.model = model;
584             return this;
585         }
586 
symbolProvider(SymbolProvider symbolProvider)587         public Builder<T> symbolProvider(SymbolProvider symbolProvider) {
588             this.symbolProvider = symbolProvider;
589             return this;
590         }
591 
protocolName(String protocolName)592         public Builder<T> protocolName(String protocolName) {
593             this.protocolName = protocolName;
594             return this;
595         }
596 
service(ServiceShape service)597         public Builder<T> service(ServiceShape service) {
598             this.service = service;
599             return this;
600         }
601 
operation(OperationShape operation)602         public Builder<T> operation(OperationShape operation) {
603             this.operation = operation;
604             return this;
605         }
606 
testCases(List<T> testCases)607         public Builder<T> testCases(List<T> testCases) {
608             this.testCases.clear();
609             return this.addTestCases(testCases);
610         }
611 
addTestCases(List<T> testCases)612         public Builder<T> addTestCases(List<T> testCases) {
613             this.testCases.addAll(testCases);
614             return this;
615         }
616 
clientConfigValue(ConfigValue configValue)617         public Builder<T> clientConfigValue(ConfigValue configValue) {
618             this.clientConfigValues.add(configValue);
619             return this;
620         }
621 
clientConfigValues(Set<ConfigValue> clientConfigValues)622         public Builder<T> clientConfigValues(Set<ConfigValue> clientConfigValues) {
623             this.clientConfigValues.clear();
624             return this.addClientConfigValues(clientConfigValues);
625         }
626 
addClientConfigValues(Set<ConfigValue> clientConfigValues)627         public Builder<T> addClientConfigValues(Set<ConfigValue> clientConfigValues) {
628             this.clientConfigValues.addAll(clientConfigValues);
629             return this;
630         }
631 
skipTest(SkipTest skipTest)632         public Builder<T> skipTest(SkipTest skipTest) {
633             this.skipTests.add(skipTest);
634             return this;
635         }
636 
skipTests(Set<SkipTest> skipTests)637         public Builder<T> skipTests(Set<SkipTest> skipTests) {
638             this.skipTests.clear();
639             return this.addSkipTests(skipTests);
640         }
641 
addSkipTests(Set<SkipTest> skipTests)642         public Builder<T> addSkipTests(Set<SkipTest> skipTests) {
643             this.skipTests.addAll(skipTests);
644             return this;
645         }
646 
shapeValueGeneratorConfig(ShapeValueGenerator.Config config)647         public Builder<T> shapeValueGeneratorConfig(ShapeValueGenerator.Config config) {
648             this.shapeValueGeneratorConfig = config;
649             return this;
650         }
651 
build()652         abstract HttpProtocolUnitTestGenerator<T> build();
653     }
654 
655     /**
656      * Represents a test client option configuration value.
657      */
658     public static class ConfigValue implements Comparable<ConfigValue> {
659         private final String name;
660         private final Consumer<GoWriter> value;
661 
ConfigValue(Builder builder)662         ConfigValue(Builder builder) {
663             this.name = SmithyBuilder.requiredState("name", builder.name);
664             this.value = SmithyBuilder.requiredState("value", builder.value);
665         }
666 
667         /**
668          * Get the config field name.
669          *
670          * @return the field name
671          */
getName()672         public String getName() {
673             return name;
674         }
675 
676         /**
677          * Get the inline value writer for the field.
678          *
679          * @return the inline value writer
680          */
getValue()681         public Consumer<GoWriter> getValue() {
682             return value;
683         }
684 
builder()685         public static Builder builder() {
686             return new Builder();
687         }
688 
689         @Override
compareTo(ConfigValue o)690         public int compareTo(ConfigValue o) {
691             return getName().compareTo(o.getName());
692         }
693 
694         @Override
equals(Object o)695         public boolean equals(Object o) {
696             if (this == o) {
697                 return true;
698             }
699             if (o == null || getClass() != o.getClass()) {
700                 return false;
701             }
702             ConfigValue that = (ConfigValue) o;
703             return Objects.equals(getName(), that.getName())
704                     && Objects.equals(getValue(), that.getValue());
705         }
706 
707         @Override
hashCode()708         public int hashCode() {
709             return Objects.hash(name, value);
710         }
711 
712         /**
713          * Builder for {@link ConfigValue}.
714          */
715         public static final class Builder implements SmithyBuilder<ConfigValue> {
716             private String name;
717             private Consumer<GoWriter> value;
718 
Builder()719             private Builder() {
720             }
721 
722             /**
723              * Set the name of the field.
724              *
725              * @param name field name
726              * @return the builder
727              */
name(String name)728             public Builder name(String name) {
729                 this.name = name;
730                 return this;
731             }
732 
733             /**
734              * Set the inline value writer.
735              *
736              * @param value the inline value writer
737              * @return the builder
738              */
value(Consumer<GoWriter> value)739             public Builder value(Consumer<GoWriter> value) {
740                 this.value = value;
741                 return this;
742             }
743 
744             @Override
build()745             public ConfigValue build() {
746                 return new ConfigValue(this);
747             }
748         }
749     }
750 
751     /**
752      * Represents a test tests that should be skipped.
753      */
754     public static class SkipTest implements Comparable<SkipTest> {
755         private final ShapeId service;
756         private final ShapeId operation;
757         private final List<String> testNames;
758 
SkipTest(Builder builder)759         SkipTest(Builder builder) {
760             this.service = SmithyBuilder.requiredState("service id", builder.service);
761             this.operation = SmithyBuilder.requiredState("operation id", builder.operation);
762             this.testNames = builder.testNames;
763         }
764 
765         /**
766          * Get the service the skip test applies to.
767          *
768          * @return the service id
769          */
getService()770         public ShapeId getService() {
771             return service;
772         }
773 
774         /**
775          * Get the operation the skip test applies to.
776          *
777          * @return the operation id
778          */
getOperation()779         public ShapeId getOperation() {
780             return operation;
781         }
782 
783         /**
784          * Get the names of the tests the skip test applies to.
785          *
786          * @return the name of the test to skip
787          */
getTestNames()788         public List<String> getTestNames() {
789             return testNames;
790         }
791 
792         /**
793          * Returns if the skip test case matches the test being evaluated. If a test name isn't specified in the skip
794          * test only the service and operation are considered for matches.
795          *
796          * @param service   id of the service
797          * @param operation id of the operation
798          * @param testName  name of the test
799          * @return if the skip test matches
800          */
matches(ShapeId service, ShapeId operation, String testName)801         public boolean matches(ShapeId service, ShapeId operation, String testName) {
802             if (!this.service.equals(service)) {
803                 return false;
804             }
805 
806             if (!this.operation.equals(operation)) {
807                 return false;
808             }
809 
810             // SkipTests not for specific test should not match this check.
811             if (this.testNames.isEmpty()) {
812                 return false;
813             }
814 
815             return this.testNames.contains(testName);
816         }
817 
818         /**
819          * Returns if the skip test matches the service and operation, with no individual test defined. If an individual
820          * test name is specified the skip test will not match.
821          *
822          * @param service   id of the service
823          * @param operation id of the operation
824          * @return if the skip test matches
825          */
matches(ShapeId service, ShapeId operation)826         public boolean matches(ShapeId service, ShapeId operation) {
827             if (!this.service.equals(service)) {
828                 return false;
829             }
830 
831             if (!this.operation.equals(operation)) {
832                 return false;
833             }
834 
835             // SkipTests for specific test should not match this check.
836             return this.testNames.isEmpty();
837         }
838 
builder()839         public static Builder builder() {
840             return new Builder();
841         }
842 
843         @Override
compareTo(SkipTest o)844         public int compareTo(SkipTest o) {
845             return Comparator.comparing(SkipTest::getService)
846                     .thenComparing(SkipTest::getOperation)
847                     .compare(this, o);
848         }
849 
850         @Override
equals(Object o)851         public boolean equals(Object o) {
852             if (this == o) {
853                 return true;
854             }
855             if (o == null || getClass() != o.getClass()) {
856                 return false;
857             }
858             SkipTest that = (SkipTest) o;
859             return Objects.equals(getService(), that.getService())
860                     && Objects.equals(getOperation(), that.getOperation())
861                     && Objects.equals(getTestNames(), that.getTestNames());
862         }
863 
864         @Override
hashCode()865         public int hashCode() {
866             return Objects.hash(service, operation, testNames);
867         }
868 
869         /**
870          * Builder for {@link SkipTest}.
871          */
872         public static final class Builder implements SmithyBuilder<SkipTest> {
873             private ShapeId service;
874             private ShapeId operation;
875             private List<String> testNames = new ArrayList<>();
876 
Builder()877             private Builder() {
878             }
879 
880             /**
881              * Set the service of the test.
882              *
883              * @param service field service
884              * @return the builder
885              */
service(ShapeId service)886             public Builder service(ShapeId service) {
887                 this.service = service;
888                 return this;
889             }
890 
891             /**
892              * Set the operation of the test.
893              *
894              * @param operation is the operation of the test to skip
895              * @return the builder
896              */
operation(ShapeId operation)897             public Builder operation(ShapeId operation) {
898                 this.operation = operation;
899                 return this;
900             }
901 
902             /**
903              * Set the name of the test to skip.
904              *
905              * @param testName is the name of the test to skip
906              * @return the builder
907              */
addTestName(String testName)908             public Builder addTestName(String testName) {
909                 this.testNames.add(testName);
910                 return this;
911             }
912 
913             @Override
build()914             public SkipTest build() {
915                 return new SkipTest(this);
916             }
917         }
918     }
919 }
920