Skip to content

Commit 15263c1

Browse files
committed
add parity test for smithy migration
1 parent 8a6a76c commit 15263c1

7 files changed

Lines changed: 888 additions & 0 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 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.awssdk.codegen.parity;
17+
18+
import java.io.File;
19+
import java.nio.file.Path;
20+
import java.nio.file.Paths;
21+
import software.amazon.awssdk.codegen.C2jModels;
22+
import software.amazon.awssdk.codegen.IntermediateModelBuilder;
23+
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
24+
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
25+
import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel;
26+
import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel;
27+
import software.amazon.awssdk.codegen.model.service.Paginators;
28+
import software.amazon.awssdk.codegen.model.service.ServiceModel;
29+
import software.amazon.awssdk.codegen.model.service.Waiters;
30+
import software.amazon.awssdk.codegen.utils.ModelLoaderUtils;
31+
32+
/**
33+
* Loads a service's codegen resources from
34+
* {@code services/<name>/src/main/resources/codegen-resources/} and builds an
35+
* {@link IntermediateModel}. Reads files in place — no copying.
36+
*/
37+
final class Fixtures {
38+
39+
private Fixtures() {
40+
}
41+
42+
static IntermediateModel buildFromC2j(String serviceName) {
43+
Path dir = servicePath(serviceName);
44+
ServiceModel service = ModelLoaderUtils.loadModel(ServiceModel.class,
45+
dir.resolve("service-2.json").toFile());
46+
47+
CustomizationConfig customization = ModelLoaderUtils
48+
.loadOptionalModel(CustomizationConfig.class, dir.resolve("customization.config").toFile())
49+
.orElseGet(CustomizationConfig::create);
50+
51+
Paginators paginators = ModelLoaderUtils
52+
.loadOptionalModel(Paginators.class, dir.resolve("paginators-1.json").toFile())
53+
.orElseGet(Paginators::none);
54+
55+
Waiters waiters = ModelLoaderUtils
56+
.loadOptionalModel(Waiters.class, dir.resolve("waiters-2.json").toFile())
57+
.orElseGet(Waiters::none);
58+
59+
EndpointRuleSetModel endpointRuleSet = ModelLoaderUtils
60+
.loadOptionalModel(EndpointRuleSetModel.class, dir.resolve("endpoint-rule-set.json").toFile())
61+
.orElse(null);
62+
63+
EndpointTestSuiteModel endpointTests = ModelLoaderUtils
64+
.loadOptionalModel(EndpointTestSuiteModel.class, dir.resolve("endpoint-tests.json").toFile())
65+
.orElse(null);
66+
67+
C2jModels models = C2jModels.builder()
68+
.serviceModel(service)
69+
.customizationConfig(customization)
70+
.paginatorsModel(paginators)
71+
.waitersModel(waiters)
72+
.endpointRuleSetModel(endpointRuleSet)
73+
.endpointTestSuiteModel(endpointTests)
74+
.build();
75+
76+
return new IntermediateModelBuilder(models).build();
77+
}
78+
79+
static IntermediateModel buildFromSmithy(String serviceName) {
80+
// TODO: swap to SmithyIntermediateModelBuilder once Smithy translation lands.
81+
return buildFromC2j(serviceName);
82+
}
83+
84+
private static Path servicePath(String serviceName) {
85+
Path dir = Paths.get("..", "services", serviceName, "src", "main", "resources", "codegen-resources");
86+
File asFile = dir.toFile();
87+
if (!asFile.exists() || !asFile.isDirectory()) {
88+
throw new IllegalArgumentException("Codegen resources directory not found: " + asFile.getAbsolutePath());
89+
}
90+
return dir;
91+
}
92+
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright 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.awssdk.codegen.parity;
17+
18+
import com.fasterxml.jackson.core.type.TypeReference;
19+
import com.fasterxml.jackson.databind.JsonNode;
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.fasterxml.jackson.databind.SerializationFeature;
22+
import com.fasterxml.jackson.databind.node.ObjectNode;
23+
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
24+
import java.io.IOException;
25+
import java.io.InputStream;
26+
import java.util.ArrayList;
27+
import java.util.Collections;
28+
import java.util.Iterator;
29+
import java.util.LinkedHashMap;
30+
import java.util.LinkedHashSet;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.Set;
34+
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
35+
36+
/**
37+
* Compares two {@link IntermediateModel}s by serializing both to JSON and
38+
* walking the trees. Emits one {@link ParityDiff} per leaf-level mismatch.
39+
* Arrays are sorted by content key before comparison so member ordering does
40+
* not produce false positives.
41+
*/
42+
final class IntermediateModelParityChecker {
43+
44+
private static final Set<String> GLOBAL_IGNORE_PATHS;
45+
46+
static {
47+
Set<String> g = new LinkedHashSet<>();
48+
GLOBAL_IGNORE_PATHS = Collections.unmodifiableSet(g);
49+
}
50+
51+
private static final String[] SORT_KEY_FIELDS = {
52+
"c2jName", "exceptionName", "name", "value"
53+
};
54+
55+
private final ObjectMapper mapper;
56+
private final List<ParityAllowlistEntry> globalIgnoreEntries;
57+
58+
IntermediateModelParityChecker() {
59+
this.mapper = new ObjectMapper()
60+
.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
61+
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
62+
.registerModule(new Jdk8Module());
63+
this.globalIgnoreEntries = new ArrayList<>(GLOBAL_IGNORE_PATHS.size());
64+
for (String path : GLOBAL_IGNORE_PATHS) {
65+
globalIgnoreEntries.add(new ParityAllowlistEntry(path, "global ignore"));
66+
}
67+
}
68+
69+
ParityResult compare(String service, IntermediateModel c2j, IntermediateModel smithy) {
70+
return compare(service, c2j, smithy, Collections.emptyList());
71+
}
72+
73+
ParityResult compare(String service,
74+
IntermediateModel c2j,
75+
IntermediateModel smithy,
76+
List<ParityAllowlistEntry> serviceAllowlist) {
77+
return compareTrees(service, mapper.valueToTree(c2j), mapper.valueToTree(smithy), serviceAllowlist);
78+
}
79+
80+
ParityResult compareTrees(String service,
81+
JsonNode c2jTree,
82+
JsonNode smithyTree,
83+
List<ParityAllowlistEntry> serviceAllowlist) {
84+
List<ParityDiff> all = new ArrayList<>();
85+
computeDiffs("", c2jTree, smithyTree, all);
86+
87+
List<ParityAllowlistEntry> combined = new ArrayList<>(globalIgnoreEntries);
88+
combined.addAll(serviceAllowlist);
89+
90+
List<ParityDiff> unexpected = new ArrayList<>(all.size());
91+
for (ParityDiff diff : all) {
92+
if (!isAllowed(diff, combined)) {
93+
unexpected.add(diff);
94+
}
95+
}
96+
return new ParityResult(service, all, unexpected);
97+
}
98+
99+
/**
100+
* Load a per-service allowlist from a JSON file.
101+
*
102+
* <p>Format: a flat JSON object mapping diff path to reason.
103+
*
104+
* <pre>{@code
105+
* {
106+
* "metadata.apiVersion": "Smithy uses compact date format"
107+
* }
108+
* }</pre>
109+
*/
110+
List<ParityAllowlistEntry> loadAllowlist(InputStream in) throws IOException {
111+
if (in == null) {
112+
return Collections.emptyList();
113+
}
114+
Map<String, String> raw = mapper.readValue(in, new TypeReference<LinkedHashMap<String, String>>() { });
115+
List<ParityAllowlistEntry> entries = new ArrayList<>(raw.size());
116+
for (Map.Entry<String, String> e : raw.entrySet()) {
117+
entries.add(new ParityAllowlistEntry(e.getKey(), e.getValue()));
118+
}
119+
return entries;
120+
}
121+
122+
private static boolean isAllowed(ParityDiff diff, List<ParityAllowlistEntry> allowlist) {
123+
for (ParityAllowlistEntry entry : allowlist) {
124+
if (entry.matches(diff.path())) {
125+
return true;
126+
}
127+
}
128+
return false;
129+
}
130+
131+
private void computeDiffs(String path, JsonNode c2j, JsonNode smithy, List<ParityDiff> out) {
132+
if (c2j == null && smithy == null) {
133+
return;
134+
}
135+
if (c2j == null || c2j.isNull()) {
136+
if (smithy != null && !smithy.isNull()) {
137+
out.add(new ParityDiff(path, ParityDiff.Type.ADDED, null, truncate(smithy.toString())));
138+
}
139+
return;
140+
}
141+
if (smithy == null || smithy.isNull()) {
142+
out.add(new ParityDiff(path, ParityDiff.Type.MISSING, truncate(c2j.toString()), null));
143+
return;
144+
}
145+
if (c2j.getNodeType() != smithy.getNodeType()) {
146+
out.add(new ParityDiff(path, ParityDiff.Type.TYPE_MISMATCH,
147+
c2j.getNodeType().name(),
148+
smithy.getNodeType().name()));
149+
return;
150+
}
151+
152+
if (c2j.isObject()) {
153+
compareObjects(path, (ObjectNode) c2j, (ObjectNode) smithy, out);
154+
} else if (c2j.isArray()) {
155+
compareArrays(path, c2j, smithy, out);
156+
} else {
157+
if (!c2j.equals(smithy)) {
158+
out.add(new ParityDiff(path, ParityDiff.Type.CHANGED,
159+
truncate(c2j.toString()),
160+
truncate(smithy.toString())));
161+
}
162+
}
163+
}
164+
165+
private void compareObjects(String path, ObjectNode c2j, ObjectNode smithy, List<ParityDiff> out) {
166+
Set<String> fields = new LinkedHashSet<>();
167+
Iterator<String> c2jFields = c2j.fieldNames();
168+
while (c2jFields.hasNext()) {
169+
fields.add(c2jFields.next());
170+
}
171+
Iterator<String> smithyFields = smithy.fieldNames();
172+
while (smithyFields.hasNext()) {
173+
fields.add(smithyFields.next());
174+
}
175+
for (String field : fields) {
176+
String childPath = path.isEmpty() ? field : path + "." + field;
177+
computeDiffs(childPath, c2j.get(field), smithy.get(field), out);
178+
}
179+
}
180+
181+
private void compareArrays(String path, JsonNode c2j, JsonNode smithy, List<ParityDiff> out) {
182+
List<JsonNode> c2jSorted = sortArray(c2j);
183+
List<JsonNode> smithySorted = sortArray(smithy);
184+
int maxLen = Math.max(c2jSorted.size(), smithySorted.size());
185+
for (int i = 0; i < maxLen; i++) {
186+
String childPath = path + "[" + i + "]";
187+
JsonNode c2jElem = i < c2jSorted.size() ? c2jSorted.get(i) : null;
188+
JsonNode smithyElem = i < smithySorted.size() ? smithySorted.get(i) : null;
189+
computeDiffs(childPath, c2jElem, smithyElem, out);
190+
}
191+
}
192+
193+
private static List<JsonNode> sortArray(JsonNode array) {
194+
List<JsonNode> sorted = new ArrayList<>();
195+
for (JsonNode element : array) {
196+
sorted.add(element);
197+
}
198+
sorted.sort((a, b) -> sortKey(a).compareTo(sortKey(b)));
199+
return sorted;
200+
}
201+
202+
private static String sortKey(JsonNode node) {
203+
if (node.isObject()) {
204+
for (String field : SORT_KEY_FIELDS) {
205+
JsonNode candidate = node.get(field);
206+
if (candidate != null && candidate.isTextual()) {
207+
return candidate.asText();
208+
}
209+
}
210+
return node.toString();
211+
}
212+
return node.asText();
213+
}
214+
215+
private static String truncate(String value) {
216+
int max = 120;
217+
if (value == null || value.length() <= max) {
218+
return value;
219+
}
220+
return value.substring(0, max) + "...(" + value.length() + " chars)";
221+
}
222+
223+
static Set<String> globalIgnorePathsForTest() {
224+
return GLOBAL_IGNORE_PATHS;
225+
}
226+
}

0 commit comments

Comments
 (0)