diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureGenerator.java index bec020563..1afd6ccc1 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureGenerator.java @@ -60,6 +60,7 @@ import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.shapes.ShapeVisitor; import software.amazon.smithy.model.shapes.ShortShape; import software.amazon.smithy.model.shapes.StringShape; @@ -400,13 +401,20 @@ private void writePropertyEqualityChecks(JavaWriter writer) { var iter = shape.members().iterator(); while (iter.hasNext()) { var member = iter.next(); + var target = model.expectShape(member.getTarget()); var memberSymbol = symbolProvider.toSymbol(member); writer.pushState(); writer.putContext("memberName", symbolProvider.toMemberName(member)); - // Use `==` instead of `equals` for unboxed primitives + // Avoid `equals` for unboxed primitives if (memberSymbol.expectProperty(SymbolProperties.IS_PRIMITIVE) && !CodegenUtils.isNullableMember(model, member)) { - writer.writeInline("this.${memberName:L} == that.${memberName:L}"); + if (target.getType() == ShapeType.FLOAT) { + writer.writeInline("Float.compare(this.${memberName:L}, that.${memberName:L}) == 0"); + } else if (target.getType() == ShapeType.DOUBLE) { + writer.writeInline("Double.compare(this.${memberName:L}, that.${memberName:L}) == 0"); + } else { + writer.writeInline("this.${memberName:L} == that.${memberName:L}"); + } } else { Class comparator = CodegenUtils.isJavaArray(memberSymbol) ? Arrays.class : Objects.class; writer.writeInline("$T.equals(this.${memberName:L}, that.${memberName:L})", comparator); diff --git a/codegen/plugins/types-codegen/build.gradle.kts b/codegen/plugins/types-codegen/build.gradle.kts index 338975e82..f4d3b0e25 100644 --- a/codegen/plugins/types-codegen/build.gradle.kts +++ b/codegen/plugins/types-codegen/build.gradle.kts @@ -7,4 +7,8 @@ description = "This module provides the codegen plugin for Smithy java type code extra["displayName"] = "Smithy :: Java :: Codegen :: Types" extra["moduleName"] = "software.amazon.smithy.java.codegen.types" +dependencies { + testImplementation(project(":codegen:test-utils")) +} + addGenerateSrcsTask("software.amazon.smithy.java.codegen.types.TestJavaTypeCodegenRunner") diff --git a/codegen/plugins/types-codegen/src/test/java/software/amazon/smithy/java/codegen/types/TypesCodegenPluginTest.java b/codegen/plugins/types-codegen/src/test/java/software/amazon/smithy/java/codegen/types/TypesCodegenPluginTest.java new file mode 100644 index 000000000..0f2bcfbf3 --- /dev/null +++ b/codegen/plugins/types-codegen/src/test/java/software/amazon/smithy/java/codegen/types/TypesCodegenPluginTest.java @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.types; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static software.amazon.smithy.java.codegen.test.PluginTestRunner.addTestCasesFromUrl; +import static software.amazon.smithy.java.codegen.test.PluginTestRunner.assertContentEquals; +import static software.amazon.smithy.java.codegen.test.PluginTestRunner.findGotContent; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.java.codegen.test.PluginTestRunner.TestCase; + +public class TypesCodegenPluginTest { + + @ParameterizedTest(name = "[{index}] => {0}") + @MethodSource("testCases") + public void runTestCase(TestCase test) { + test.builder().build(); + var got = test.manifests().stream().flatMap(x -> x.getFiles().stream()).collect(Collectors.toSet()); + for (var expected : test.expectedToContents().keySet()) { + var found = findExpected(expected, got); + assertNotNull(found); + var contents = findGotContent(found, test); + assertTrue(contents.isPresent()); + assertContentEquals(test.expectedToContents().get(expected), contents.get().trim()); + } + } + + //@ParameterizedTest(name = "[{index}] => {0}") + //@MethodSource("testCases") + // This test can be used to render the java files when we there are changes to the codegen logic. + public void renderExpected(TestCase test) throws IOException { + test.builder().build(); + var got = test.manifests().stream().flatMap(x -> x.getFiles().stream()).collect(Collectors.toSet()); + for (var expected : test.expectedToContents().keySet()) { + var found = findExpected(expected, got); + assertNotNull(found); + var contents = findGotContent(found, test); + var expectedFile = new File("/tmp/rendered/" + test + "/expected" + expected); + expectedFile.getParentFile().mkdirs(); + Files.write(expectedFile.toPath(), contents.get().getBytes(StandardCharsets.UTF_8)); + } + } + + private Path findExpected(String expected, Set manifestFiles) { + return manifestFiles.stream().filter(path -> path.toString().contains(expected)).findFirst().orElse(null); + } + + public static Collection testCases() { + return addTestCasesFromUrl(TypesCodegenPluginTest.class.getResource("test-cases")); + } +} diff --git a/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/expected/StructureOne.java b/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/expected/StructureOne.java new file mode 100644 index 000000000..47cc9cd78 --- /dev/null +++ b/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/expected/StructureOne.java @@ -0,0 +1,330 @@ +package software.amazon.smithy.java.example.standalone.model; + +import java.util.Objects; +import software.amazon.smithy.java.core.schema.PresenceTracker; +import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.core.schema.SchemaUtils; +import software.amazon.smithy.java.core.schema.SerializableStruct; +import software.amazon.smithy.java.core.schema.ShapeBuilder; +import software.amazon.smithy.java.core.serde.ShapeDeserializer; +import software.amazon.smithy.java.core.serde.ShapeSerializer; +import software.amazon.smithy.java.core.serde.ToStringSerializer; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.SmithyGenerated; + +@SmithyGenerated +public final class StructureOne implements SerializableStruct { + + public static final Schema $SCHEMA = Schemas.STRUCTURE_ONE; + private static final Schema $SCHEMA_BYTE_MEMBER = $SCHEMA.member("byte"); + private static final Schema $SCHEMA_SHORT_MEMBER = $SCHEMA.member("short"); + private static final Schema $SCHEMA_INT_MEMBER = $SCHEMA.member("int"); + private static final Schema $SCHEMA_LONG_MEMBER = $SCHEMA.member("long"); + private static final Schema $SCHEMA_FLOAT_MEMBER = $SCHEMA.member("float"); + private static final Schema $SCHEMA_DOUBLE_MEMBER = $SCHEMA.member("double"); + private static final Schema $SCHEMA_BOOLEAN_MEMBER = $SCHEMA.member("boolean"); + + public static final ShapeId $ID = $SCHEMA.id(); + + private final transient byte byteMember; + private final transient short shortMember; + private final transient int intMember; + private final transient long longMember; + private final transient float floatMember; + private final transient double doubleMember; + private final transient boolean booleanMember; + + private StructureOne(Builder builder) { + this.byteMember = builder.byteMember; + this.shortMember = builder.shortMember; + this.intMember = builder.intMember; + this.longMember = builder.longMember; + this.floatMember = builder.floatMember; + this.doubleMember = builder.doubleMember; + this.booleanMember = builder.booleanMember; + } + + public byte getByte() { + return byteMember; + } + + public short getShort() { + return shortMember; + } + + public int getInt() { + return intMember; + } + + public long getLong() { + return longMember; + } + + public float getFloat() { + return floatMember; + } + + public double getDouble() { + return doubleMember; + } + + public boolean isBoolean() { + return booleanMember; + } + + @Override + public String toString() { + return ToStringSerializer.serialize(this); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + StructureOne that = (StructureOne) other; + return this.byteMember == that.byteMember + && this.shortMember == that.shortMember + && this.intMember == that.intMember + && this.longMember == that.longMember + && Float.compare(this.floatMember, that.floatMember) == 0 + && Double.compare(this.doubleMember, that.doubleMember) == 0 + && this.booleanMember == that.booleanMember; + } + + @Override + public int hashCode() { + return Objects.hash(byteMember, shortMember, intMember, longMember, floatMember, doubleMember, booleanMember); + } + + @Override + public Schema schema() { + return $SCHEMA; + } + + @Override + public void serializeMembers(ShapeSerializer serializer) { + serializer.writeByte($SCHEMA_BYTE_MEMBER, byteMember); + serializer.writeShort($SCHEMA_SHORT_MEMBER, shortMember); + serializer.writeInteger($SCHEMA_INT_MEMBER, intMember); + serializer.writeLong($SCHEMA_LONG_MEMBER, longMember); + serializer.writeFloat($SCHEMA_FLOAT_MEMBER, floatMember); + serializer.writeDouble($SCHEMA_DOUBLE_MEMBER, doubleMember); + serializer.writeBoolean($SCHEMA_BOOLEAN_MEMBER, booleanMember); + } + + @Override + @SuppressWarnings("unchecked") + public T getMemberValue(Schema member) { + return switch (member.memberIndex()) { + case 0 -> (T) SchemaUtils.validateSameMember($SCHEMA_BYTE_MEMBER, member, byteMember); + case 1 -> (T) SchemaUtils.validateSameMember($SCHEMA_SHORT_MEMBER, member, shortMember); + case 2 -> (T) SchemaUtils.validateSameMember($SCHEMA_INT_MEMBER, member, intMember); + case 3 -> (T) SchemaUtils.validateSameMember($SCHEMA_LONG_MEMBER, member, longMember); + case 4 -> (T) SchemaUtils.validateSameMember($SCHEMA_FLOAT_MEMBER, member, floatMember); + case 5 -> (T) SchemaUtils.validateSameMember($SCHEMA_DOUBLE_MEMBER, member, doubleMember); + case 6 -> (T) SchemaUtils.validateSameMember($SCHEMA_BOOLEAN_MEMBER, member, booleanMember); + default -> throw new IllegalArgumentException("Attempted to get non-existent member: " + member.id()); + }; + } + + /** + * Create a new builder containing all the current property values of this object. + * + *

Note: This method performs only a shallow copy of the original properties. + * + * @return a builder for {@link StructureOne}. + */ + public Builder toBuilder() { + var builder = new Builder(); + builder.byteMember(this.byteMember); + builder.shortMember(this.shortMember); + builder.intMember(this.intMember); + builder.longMember(this.longMember); + builder.floatMember(this.floatMember); + builder.doubleMember(this.doubleMember); + builder.booleanMember(this.booleanMember); + return builder; + } + + /** + * @return returns a new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link StructureOne}. + */ + public static final class Builder implements ShapeBuilder { + private final PresenceTracker tracker = PresenceTracker.of($SCHEMA); + private byte byteMember; + private short shortMember; + private int intMember; + private long longMember; + private float floatMember; + private double doubleMember; + private boolean booleanMember; + + private Builder() {} + + @Override + public Schema schema() { + return $SCHEMA; + } + + /** + *

Required + * @return this builder. + */ + public Builder byteMember(byte byteMember) { + this.byteMember = byteMember; + tracker.setMember($SCHEMA_BYTE_MEMBER); + return this; + } + + /** + *

Required + * @return this builder. + */ + public Builder shortMember(short shortMember) { + this.shortMember = shortMember; + tracker.setMember($SCHEMA_SHORT_MEMBER); + return this; + } + + /** + *

Required + * @return this builder. + */ + public Builder intMember(int intMember) { + this.intMember = intMember; + tracker.setMember($SCHEMA_INT_MEMBER); + return this; + } + + /** + *

Required + * @return this builder. + */ + public Builder longMember(long longMember) { + this.longMember = longMember; + tracker.setMember($SCHEMA_LONG_MEMBER); + return this; + } + + /** + *

Required + * @return this builder. + */ + public Builder floatMember(float floatMember) { + this.floatMember = floatMember; + tracker.setMember($SCHEMA_FLOAT_MEMBER); + return this; + } + + /** + *

Required + * @return this builder. + */ + public Builder doubleMember(double doubleMember) { + this.doubleMember = doubleMember; + tracker.setMember($SCHEMA_DOUBLE_MEMBER); + return this; + } + + /** + *

Required + * @return this builder. + */ + public Builder booleanMember(boolean booleanMember) { + this.booleanMember = booleanMember; + tracker.setMember($SCHEMA_BOOLEAN_MEMBER); + return this; + } + + @Override + public StructureOne build() { + tracker.validate(); + return new StructureOne(this); + } + + @Override + @SuppressWarnings("unchecked") + public void setMemberValue(Schema member, Object value) { + switch (member.memberIndex()) { + case 0 -> byteMember((byte) SchemaUtils.validateSameMember($SCHEMA_BYTE_MEMBER, member, value)); + case 1 -> shortMember((short) SchemaUtils.validateSameMember($SCHEMA_SHORT_MEMBER, member, value)); + case 2 -> intMember((int) SchemaUtils.validateSameMember($SCHEMA_INT_MEMBER, member, value)); + case 3 -> longMember((long) SchemaUtils.validateSameMember($SCHEMA_LONG_MEMBER, member, value)); + case 4 -> floatMember((float) SchemaUtils.validateSameMember($SCHEMA_FLOAT_MEMBER, member, value)); + case 5 -> doubleMember((double) SchemaUtils.validateSameMember($SCHEMA_DOUBLE_MEMBER, member, value)); + case 6 -> booleanMember((boolean) SchemaUtils.validateSameMember($SCHEMA_BOOLEAN_MEMBER, member, value)); + default -> ShapeBuilder.super.setMemberValue(member, value); + } + } + + @Override + public ShapeBuilder errorCorrection() { + if (tracker.allSet()) { + return this; + } + if (!tracker.checkMember($SCHEMA_BYTE_MEMBER)) { + tracker.setMember($SCHEMA_BYTE_MEMBER); + } + if (!tracker.checkMember($SCHEMA_SHORT_MEMBER)) { + tracker.setMember($SCHEMA_SHORT_MEMBER); + } + if (!tracker.checkMember($SCHEMA_INT_MEMBER)) { + tracker.setMember($SCHEMA_INT_MEMBER); + } + if (!tracker.checkMember($SCHEMA_LONG_MEMBER)) { + tracker.setMember($SCHEMA_LONG_MEMBER); + } + if (!tracker.checkMember($SCHEMA_FLOAT_MEMBER)) { + tracker.setMember($SCHEMA_FLOAT_MEMBER); + } + if (!tracker.checkMember($SCHEMA_DOUBLE_MEMBER)) { + tracker.setMember($SCHEMA_DOUBLE_MEMBER); + } + if (!tracker.checkMember($SCHEMA_BOOLEAN_MEMBER)) { + tracker.setMember($SCHEMA_BOOLEAN_MEMBER); + } + return this; + } + + @Override + public Builder deserialize(ShapeDeserializer decoder) { + decoder.readStruct($SCHEMA, this, $InnerDeserializer.INSTANCE); + return this; + } + + @Override + public Builder deserializeMember(ShapeDeserializer decoder, Schema schema) { + decoder.readStruct(schema.assertMemberTargetIs($SCHEMA), this, $InnerDeserializer.INSTANCE); + return this; + } + + private static final class $InnerDeserializer implements ShapeDeserializer.StructMemberConsumer { + private static final $InnerDeserializer INSTANCE = new $InnerDeserializer(); + + @Override + public void accept(Builder builder, Schema member, ShapeDeserializer de) { + switch (member.memberIndex()) { + case 0 -> builder.byteMember(de.readByte(member)); + case 1 -> builder.shortMember(de.readShort(member)); + case 2 -> builder.intMember(de.readInteger(member)); + case 3 -> builder.longMember(de.readLong(member)); + case 4 -> builder.floatMember(de.readFloat(member)); + case 5 -> builder.doubleMember(de.readDouble(member)); + case 6 -> builder.booleanMember(de.readBoolean(member)); + default -> throw new IllegalArgumentException("Unexpected member: " + member.memberName()); + } + } + } + } +} diff --git a/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/model/main.smithy b/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/model/main.smithy new file mode 100644 index 000000000..e0f13c9b6 --- /dev/null +++ b/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/model/main.smithy @@ -0,0 +1,20 @@ +$version: "2" + +namespace smithy.test + +structure StructureOne { + @required + byte: Byte + @required + short: Short + @required + int: Integer + @required + long: Long + @required + float: Float + @required + double: Double + @required + boolean: Boolean +} diff --git a/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/smithy-build.json b/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/smithy-build.json new file mode 100644 index 000000000..18f2e820d --- /dev/null +++ b/codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/primitive-types/smithy-build.json @@ -0,0 +1,8 @@ +{ + "version": "1.0", + "plugins": { + "java-type-codegen": { + "namespace": "software.amazon.smithy.java.example.standalone" + } + } +} diff --git a/codegen/test-utils/build.gradle.kts b/codegen/test-utils/build.gradle.kts new file mode 100644 index 000000000..700da3634 --- /dev/null +++ b/codegen/test-utils/build.gradle.kts @@ -0,0 +1,15 @@ + +plugins { + id("smithy-java.module-conventions") +} + +description = "This module provides utilities for testing codegen plugins" + +extra["displayName"] = "Smithy :: Java :: Codegen :: Test" +extra["moduleName"] = "software.amazon.smithy.java.codegen.test" + +dependencies { + implementation(libs.smithy.codegen) + api(platform(libs.junit.bom)) + api(libs.junit.jupiter.api) +} diff --git a/codegen/test-utils/src/main/java/software/amazon/smithy/java/codegen/test/PluginTestRunner.java b/codegen/test-utils/src/main/java/software/amazon/smithy/java/codegen/test/PluginTestRunner.java new file mode 100644 index 000000000..26fb2ae15 --- /dev/null +++ b/codegen/test-utils/src/main/java/software/amazon/smithy/java/codegen/test/PluginTestRunner.java @@ -0,0 +1,258 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.SmithyBuild; +import software.amazon.smithy.build.model.SmithyBuildConfig; +import software.amazon.smithy.model.Model; + +public class PluginTestRunner { + + private PluginTestRunner() {} + + private static final Predicate NOT_COMMENT = Predicate.not(Pattern.compile("^ *//.*$").asMatchPredicate()); + + public static Optional findGotContent(Path found, TestCase test) { + for (var manifest : test.manifests) { + var fileInsideBaseDir = + new File(found.toString().replace(manifest.getBaseDir().toString() + "/", "")).toPath(); + var contents = manifest.getFileString(fileInsideBaseDir); + if (contents.isPresent()) { + return contents; + } + } + return Optional.empty(); + } + + public static void assertContentEquals(String left, String right) { + try { + assertEquals(normalizeSpace(left), normalizeSpace(right)); + } catch (Throwable e) { + assertEquals(left, right); + } + } + + private static String normalizeSpace(String value) { + return value + .replaceAll("\n +", "\n") + .replaceAll(" +\n", "\n"); + } + + public static Path findExpected(String expected, Set manifestFiles) { + return manifestFiles.stream().filter(path -> path.toString().contains(expected)).findFirst().orElse(null); + } + + public static List addTestCasesFromUrl(URL url) { + if (!"file".equals(url.getProtocol())) { + throw new IllegalArgumentException("Only file URLs are supported: " + url); + } + try { + return addTestCasesFromDirectory(Paths.get(url.toURI())); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private static List addTestCasesFromDirectory(Path rootDir) { + try (Stream files = Files.walk(rootDir, 1)) { + var testCases = new ArrayList(); + files.map(Path::toFile) + .filter(File::isDirectory) + .filter(dir -> new File(dir, "smithy-build.json").exists()) + .map(PluginTestRunner::fromDirectory) + .forEach(testCases::add); + return testCases; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static TestCase fromDirectory(File dir) { + var configFile = new File(dir, "smithy-build.json"); + var modelDir = new File(dir, "model"); + var model = Model.assembler() + .addImport(modelDir.toPath()) + .discoverModels() + .assemble() + .unwrap(); + var manifests = new ArrayList(); + Function fileManifestFactory = pluginBaseDir -> { + var fileManifest = new MockManifest(pluginBaseDir); + manifests.add(fileManifest); + return fileManifest; + }; + var config = SmithyBuildConfig.builder() + .load(configFile.toPath()) + .outputDirectory("build") + .build(); + var builder = new SmithyBuild() + .fileManifestFactory(fileManifestFactory) + .config(config) + .model(model); + var javaFiles = new ArrayList(); + var expectedDir = new File(dir, "expected").toPath(); + + try { + Files.walkFileTree(expectedDir, new JavaFileVisitor(javaFiles)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + var expectedToContents = getExpectedContents(expectedDir, javaFiles); + return builder() + .name(dir.toPath().getFileName().toString()) + .builder(builder) + .manifests(manifests) + .expectedToContents(expectedToContents) + .build(); + } + + static Map getExpectedContents(Path base, List paths) { + var prefix = base.toString(); + var result = new HashMap(); + try { + for (var path : paths) { + var relative = path.toString().replace(prefix, ""); + var contents = Files.readString(path); + result.put(relative, removeSingleLineComments(contents)); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return result; + } + + /** + * Removes single line comments that can be added to the expected class to annotate specific behavior. + */ + private static String removeSingleLineComments(String contents) { + return Arrays.asList(contents.split("\\n")) + .stream() + .filter(NOT_COMMENT) + .collect(Collectors.joining("\n")) + .trim(); + } + + public static TestCaseBuilder builder() { + return new TestCaseBuilder(); + } + + public static class TestCase { + private final String name; + private final SmithyBuild builder; + private final List manifests; + private final Map expectedToContents; + + TestCase(TestCaseBuilder builder) { + this.name = Objects.requireNonNull(builder.name, "name"); + this.builder = Objects.requireNonNull(builder.builder, "builder"); + this.manifests = Objects.requireNonNull(builder.manifests, "manifest"); + this.expectedToContents = Objects.requireNonNull(builder.expectedToContents, "expectedToContents"); + } + + public String name() { + return name; + } + + public SmithyBuild builder() { + return builder; + } + + public List manifests() { + return manifests; + } + + public Map expectedToContents() { + return expectedToContents; + } + + @Override + public String toString() { + return name; + } + } + + public static class TestCaseBuilder { + private String name; + private SmithyBuild builder; + private List manifests; + private Map expectedToContents; + + public TestCaseBuilder name(String name) { + this.name = name; + return this; + } + + public TestCaseBuilder manifests(List manifests) { + this.manifests = manifests; + return this; + } + + public TestCaseBuilder builder(SmithyBuild builder) { + this.builder = builder; + return this; + } + + public TestCaseBuilder expectedToContents(Map expectedToContents) { + this.expectedToContents = expectedToContents; + return this; + } + + public TestCase build() { + return new TestCase(this); + } + } + + public static class JavaFileVisitor extends SimpleFileVisitor { + private final List javaFiles; + + public JavaFileVisitor(List javaFiles) { + this.javaFiles = javaFiles; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (Files.isRegularFile(file) && file.toString().endsWith(".java")) { + javaFiles.add(file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + // Handle the error and continue + exc.printStackTrace(); + return FileVisitResult.CONTINUE; + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b35039a70..f631dea78 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -60,6 +60,7 @@ include(":codegen:plugins") include(":codegen:plugins:client-codegen") include(":codegen:plugins:server-codegen") include(":codegen:plugins:types-codegen") +include(":codegen:test-utils") // Utilities include(":jmespath") @@ -110,4 +111,4 @@ include(":mcp:mcp-cli-api") include(":mcp:mcp-bundle-api") include(":model-bundle") -include(":model-bundle:model-bundle-api") \ No newline at end of file +include(":model-bundle:model-bundle-api")