From c66217f8ecfffb7e3f6dcf888061a2a94e3f5a49 Mon Sep 17 00:00:00 2001 From: Sean McGrail Date: Tue, 6 Apr 2021 15:32:30 -0700 Subject: [PATCH] Add support for creating a manifest description of generated artifacts. --- .../smithy/go/codegen/CodegenVisitor.java | 5 +- .../smithy/go/codegen/GoDependency.java | 29 ++ .../smithy/go/codegen/GoModGenerator.java | 23 +- .../smithy/go/codegen/ManifestWriter.java | 132 ++++++ .../smithy/go/codegen/SemanticVersion.java | 382 ++++++++++++++++++ .../go/codegen/SemanticVersionTest.java | 134 ++++++ 6 files changed, 693 insertions(+), 12 deletions(-) create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ManifestWriter.java create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SemanticVersion.java create mode 100644 codegen/smithy-go-codegen/src/test/java/software/amazon/smithy/go/codegen/SemanticVersionTest.java diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java index a70e18140..f57113b33 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java @@ -219,7 +219,10 @@ void execute() { writers.flushWriters(); LOGGER.fine("Generating go.mod file"); - GoModGenerator.writeGoMod(settings, fileManifest, SymbolDependency.gatherDependencies(dependencies.stream())); + GoModGenerator.writeGoMod(settings, fileManifest, dependencies); + + LOGGER.fine("Generating build manifest file"); + ManifestWriter.writeManifest(settings, fileManifest, dependencies); LOGGER.fine("Running go fmt"); CodegenUtils.runCommand("gofmt -w -s .", fileManifest.getBaseDir()); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoDependency.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoDependency.java index ad8a17100..882c1986d 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoDependency.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoDependency.java @@ -21,6 +21,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.SymbolDependency; import software.amazon.smithy.codegen.core.SymbolDependencyContainer; import software.amazon.smithy.utils.SetUtils; @@ -54,6 +55,34 @@ private GoDependency(Builder builder) { .build(); } + /** + * Given two {@link SymbolDependency} referring to the same package, return the minimum dependency version using + * minimum version selection. The version strings must be semver compatible. + * + * @param dx the first dependency + * @param dy the second dependency + * @return the minimum dependency + */ + public static SymbolDependency mergeByMinimumVersionSelection(SymbolDependency dx, SymbolDependency dy) { + SemanticVersion sx = SemanticVersion.parseVersion(dx.getVersion()); + SemanticVersion sy = SemanticVersion.parseVersion(dy.getVersion()); + + // This *shouldn't* happen in Go since the Go module import path must end with the major version component. + // Exception is the case where the major version is 0 or 1. + if (sx.getMajor() != sy.getMajor() && !(sx.getMajor() == 0 || sy.getMajor() == 0)) { + throw new CodegenException(String.format("Dependency %s has conflicting major versions", + dx.getPackageName())); + } + + int cmp = sx.compareTo(sy); + if (cmp < 0) { + return dy; + } else if (cmp > 0) { + return dx; + } + return dx; + } + /** * Get the the set of {@link GoDependency} required by this dependency. * diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoModGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoModGenerator.java index 79f2a9852..1ec793fcb 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoModGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoModGenerator.java @@ -18,8 +18,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Map; -import java.util.TreeMap; import java.util.stream.Collectors; import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.codegen.core.CodegenException; @@ -32,12 +32,13 @@ */ final class GoModGenerator { - private GoModGenerator() {} + private GoModGenerator() { + } static void writeGoMod( GoSettings settings, FileManifest manifest, - Map> dependencies + List dependencies ) { Path goModFile = manifest.getBaseDir().resolve("go.mod"); @@ -51,6 +52,8 @@ static void writeGoMod( throw new CodegenException("Failed to delete existing go.mod file", e); } } + manifest.addFile(goModFile); + CodegenUtils.runCommand("go mod init " + settings.getModuleName(), manifest.getBaseDir()); Map externalDependencies = getExternalDependencies(dependencies); @@ -61,13 +64,11 @@ static void writeGoMod( } } - private static Map getExternalDependencies( - Map> dependencies - ) { - return dependencies.entrySet().stream() - .filter(entry -> !entry.getKey().equals("stdlib")) - .flatMap(entry -> entry.getValue().entrySet().stream()) - .collect(Collectors.toMap( - Map.Entry::getKey, entry -> entry.getValue().getVersion(), (a, b) -> b, TreeMap::new)); + private static Map getExternalDependencies(List dependencies) { + return SymbolDependency.gatherDependencies(dependencies.stream() + .filter(s -> !s.getDependencyType().equals(GoDependency.Type.STANDARD_LIBRARY.toString())), + GoDependency::mergeByMinimumVersionSelection) + .entrySet().stream().flatMap(e -> e.getValue().entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getVersion())); } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ManifestWriter.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ManifestWriter.java new file mode 100644 index 000000000..f42d038d5 --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ManifestWriter.java @@ -0,0 +1,132 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.go.codegen; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.SymbolDependency; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; + +/** + * Generates a manifest description of the generated code, minimum go version, and minimum dependencies required. + */ +public final class ManifestWriter { + private static final String GENERATED_JSON = "generated.json"; + + private ManifestWriter() { + } + + /** + * Write the manifest description of the generated code. + * + * @param settings the go settings + * @param fileManifest the file manifest + * @param dependencies the list of symbol dependencies + */ + public static void writeManifest( + GoSettings settings, + FileManifest fileManifest, + List dependencies + ) { + Path manifestFile = fileManifest.getBaseDir().resolve(GENERATED_JSON); + + if (Files.exists(manifestFile)) { + try { + Files.delete(manifestFile); + } catch (IOException e) { + throw new CodegenException("Failed to delete existing " + GENERATED_JSON + " file", e); + } + } + fileManifest.addFile(manifestFile); + + Node generatedJson = buildManifestFile(settings, fileManifest, dependencies); + fileManifest.writeFile(manifestFile.toString(), Node.prettyPrintJson(generatedJson)); + } + + private static Node buildManifestFile( + GoSettings settings, + FileManifest fileManifest, + List dependencies + ) { + List nonStdLib = new ArrayList<>(); + Optional minStandard = Optional.empty(); + + for (SymbolDependency dependency : dependencies) { + if (!dependency.getDependencyType().equals(GoDependency.Type.STANDARD_LIBRARY.toString())) { + nonStdLib.add(dependency); + } else { + if (minStandard.isPresent()) { + if (minStandard.get().getVersion().compareTo(dependency.getVersion()) < 0) { + minStandard = Optional.of(dependency); + } + } else { + minStandard = Optional.of(dependency); + } + } + } + + Map manifestNodes = new HashMap<>(); + + Map minimumDependencies = gatherMinimumDependencies(nonStdLib.stream()); + + Map dependencyNodes = new HashMap<>(); + for (Map.Entry entry : minimumDependencies.entrySet()) { + dependencyNodes.put(StringNode.from(entry.getKey()), + StringNode.from(entry.getValue())); + } + + Collection generatedFiles = new ArrayList<>(); + Path baseDir = fileManifest.getBaseDir(); + for (Path filePath : fileManifest.getFiles()) { + generatedFiles.add(baseDir.relativize(filePath).toString()); + } + generatedFiles = generatedFiles.stream().sorted().collect(Collectors.toList()); + + manifestNodes.put(StringNode.from("module"), StringNode.from(settings.getModuleName())); + minStandard.ifPresent(symbolDependency -> + manifestNodes.put(StringNode.from("go"), StringNode.from(symbolDependency.getVersion()))); + manifestNodes.put(StringNode.from("dependencies"), ObjectNode.objectNode(dependencyNodes)); + manifestNodes.put(StringNode.from("files"), ArrayNode.fromStrings(generatedFiles)); + + return ObjectNode.objectNode(manifestNodes).withDeepSortedKeys(); + } + + private static Map gatherMinimumDependencies( + Stream symbolStream + ) { + return SymbolDependency.gatherDependencies(symbolStream, GoDependency::mergeByMinimumVersionSelection) + .entrySet().stream() + .flatMap(entry -> entry.getValue().entrySet().stream()) + .collect(Collectors.toMap( + Map.Entry::getKey, entry -> entry.getValue().getVersion(), (a, b) -> b, TreeMap::new)); + } + +} diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SemanticVersion.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SemanticVersion.java new file mode 100644 index 000000000..bc55c1604 --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SemanticVersion.java @@ -0,0 +1,382 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.go.codegen; + +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import software.amazon.smithy.utils.SmithyBuilder; + +/** + * A semantic version parser that allows for prefixes to be compatible with Go version tags. + */ +public final class SemanticVersion { + // Regular Expression from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + private static final Pattern SEMVER_PATTERN = Pattern.compile("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" + + "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"); + + private final String prefix; + private final int major; + private final int minor; + private final int patch; + private final String preRelease; + private final String build; + + private SemanticVersion(Builder builder) { + prefix = builder.prefix; + major = builder.major; + minor = builder.minor; + patch = builder.patch; + preRelease = builder.preRelease; + build = builder.build; + } + + /** + * The semantic version prefix present before the major version. + * + * @return the optional prefix + */ + public Optional getPrefix() { + return Optional.ofNullable(prefix); + } + + /** + * The major version number. + * + * @return the major version + */ + public int getMajor() { + return major; + } + + /** + * The minor version number. + * + * @return the minor version + */ + public int getMinor() { + return minor; + } + + /** + * The patch version number. + * + * @return the patch version + */ + public int getPatch() { + return patch; + } + + public Optional getPreRelease() { + return Optional.ofNullable(preRelease); + } + + public Optional getBuild() { + return Optional.ofNullable(build); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + if (getPrefix().isPresent()) { + builder.append(getPrefix().get()); + } + + builder.append(getMajor()); + builder.append('.'); + builder.append(getMinor()); + builder.append('.'); + builder.append(getPatch()); + if (getPreRelease().isPresent()) { + builder.append('-'); + builder.append(getPreRelease().get()); + } + if (getBuild().isPresent()) { + builder.append('+'); + builder.append(getBuild().get()); + } + + return builder.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SemanticVersion that = (SemanticVersion) o; + return getMajor() == that.getMajor() + && getMinor() == that.getMinor() + && getPatch() == that.getPatch() + && getPrefix().equals(that.getPrefix()) + && getPreRelease().equals(that.getPreRelease()) + && getBuild().equals(that.getBuild()); + } + + @Override + public int hashCode() { + return Objects.hash(getPrefix(), getMajor(), getMinor(), getPatch(), getPreRelease(), getBuild()); + } + + /** + * Parse a semantic version string into a {@link SemanticVersion}. + * + * @param version the semantic version string + * @return the SemanticVersion representing the parsed value + */ + public static SemanticVersion parseVersion(String version) { + char[] parseArr = version.toCharArray(); + StringBuilder prefixBuilder = new StringBuilder(); + int position = 0; + while (position < parseArr.length && !Character.isDigit(parseArr[position])) { + prefixBuilder.append(parseArr[position]); + position++; + } + + String prefix = null; + if (prefixBuilder.length() > 0) { + prefix = prefixBuilder.toString(); + } + + Matcher matcher = SEMVER_PATTERN.matcher(version.substring(position)); + + if (!matcher.matches()) { + throw newInvalidSemanticVersion(version); + } + + return builder() + .prefix(prefix) + .major(Integer.parseInt(matcher.group(1))) + .minor(Integer.parseInt(matcher.group(2))) + .patch(Integer.parseInt(matcher.group(3))) + .preRelease(matcher.group(4)) + .build(matcher.group(5)) + .build(); + } + + private static IllegalArgumentException newInvalidSemanticVersion(String version) { + return new IllegalArgumentException("Invalid semantic version string: " + version); + } + + /** + * Get a {@link SemanticVersion} builder. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return a builder for this {@link SemanticVersion}. + * + * @return the builder + */ + public Builder toBuilder() { + return builder() + .prefix(this.prefix) + .major(this.major) + .minor(this.minor) + .patch(this.patch) + .preRelease(this.preRelease) + .build(this.build); + } + + /** + * Compare two {@link SemanticVersion}, ignoring prefix strings. To validate that prefix strings match + * see the overloaded function signature. + * + * @param o the {@link SemanticVersion} to be compared. + * @return the value {@code 0} if this {@code SemanticVersion} is + * equal to the argument {@code SemanticVersion}; a value less than + * {@code 0} if this {@code SemanticVersion} is less + * than the argument {@code SemanticVersion}; and a value greater + * than {@code 0} if this {@code SemanticVersion} is + * greater than the argument {@code SemanticVersion}. + */ + public int compareTo(SemanticVersion o) { + return compareTo(o, (o1, o2) -> 0); + } + + /** + * Compare two {@link SemanticVersion}, using the prefixComparator for comparing the prefix strings. + * + * @param o the {@link SemanticVersion} to be compared. + * @param prefixComparator the comparator for comparing prefixes + * @return the value {@code 0} if this {@code SemanticVersion} is + * equal to the argument {@code SemanticVersion}; a value less than + * {@code 0} if this {@code SemanticVersion} is less + * than the argument {@code SemanticVersion}; and a value greater + * than {@code 0} if this {@code SemanticVersion} is + * greater than the argument {@code SemanticVersion}. + */ + public int compareTo( + SemanticVersion o, + Comparator> prefixComparator + ) { + int cmp = prefixComparator.compare(getPrefix(), o.getPrefix()); + if (cmp != 0) { + return cmp; + } + + cmp = Integer.compare(getMajor(), o.getMajor()); + if (cmp != 0) { + return cmp; + } + + cmp = Integer.compare(getMinor(), o.getMinor()); + if (cmp != 0) { + return cmp; + } + + cmp = Integer.compare(getPatch(), o.getPatch()); + if (cmp != 0) { + return cmp; + } + + if (!getPreRelease().isPresent() && !o.getPreRelease().isPresent()) { + return 0; + } + + if (!getPreRelease().isPresent()) { + return 1; + } + + if (!o.getPreRelease().isPresent()) { + return -1; + } + + return comparePreRelease(getPreRelease().get(), o.getPreRelease().get()); + } + + private static int comparePreRelease(String x, String y) { + String[] xIdentifiers = x.split("\\."); + String[] yIdentifiers = y.split("\\."); + + int cmp = 0; + int xPos = 0; + int yPos = 0; + + while (xPos < xIdentifiers.length && yPos < yIdentifiers.length && cmp == 0) { + Optional xInt = parsePositiveInteger(xIdentifiers[xPos]); + Optional yInt = parsePositiveInteger(yIdentifiers[yPos]); + + if (xInt.isPresent() && yInt.isPresent()) { + cmp = Integer.compare(xInt.get(), yInt.get()); + continue; + } + + if (xInt.isPresent()) { + cmp = -1; + continue; + } + + if (yInt.isPresent()) { + cmp = 1; + continue; + } + + cmp = xIdentifiers[xPos].compareTo(yIdentifiers[yPos]); + + xPos++; + yPos++; + } + + if (cmp != 0) { + return cmp; + } + + int xRemaining = xIdentifiers.length - 1 - xPos; + int yRemaining = yIdentifiers.length - 1 - yPos; + + if (xRemaining == yRemaining) { + return 0; + } + + return (xRemaining < yRemaining) ? -1 : 1; + } + + private static Optional parsePositiveInteger(String value) { + try { + int i = Integer.parseInt(value); + + if (i < 0) { + return Optional.empty(); + } + + return Optional.of(i); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + /** + * Builder for {@link SemanticVersion}. + */ + public static final class Builder implements SmithyBuilder { + private String prefix; + private int major; + private int minor; + private int patch; + private String preRelease; + private String build; + + private Builder() { + } + + public Builder prefix(String prefix) { + this.prefix = prefix; + return this; + } + + public Builder major(int major) { + this.major = major; + return this; + } + + public Builder minor(int minor) { + this.minor = minor; + return this; + } + + public Builder patch(int patch) { + this.patch = patch; + return this; + } + + public Builder preRelease(String preRelease) { + this.preRelease = preRelease; + return this; + } + + public Builder build(String build) { + this.build = build; + return this; + } + + @Override + public SemanticVersion build() { + return new SemanticVersion(this); + } + } +} diff --git a/codegen/smithy-go-codegen/src/test/java/software/amazon/smithy/go/codegen/SemanticVersionTest.java b/codegen/smithy-go-codegen/src/test/java/software/amazon/smithy/go/codegen/SemanticVersionTest.java new file mode 100644 index 000000000..e0a1aed63 --- /dev/null +++ b/codegen/smithy-go-codegen/src/test/java/software/amazon/smithy/go/codegen/SemanticVersionTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.go.codegen; + +import static org.hamcrest.MatcherAssert.assertThat; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +public class SemanticVersionTest { + + @Test + public void testSemanticVersion() { + SemanticVersion version = SemanticVersion.parseVersion("1.2.3"); + assertThat(version.toString(), Matchers.equalTo("1.2.3")); + } + + @Test + public void testSemanticVersionWithPrefix() { + SemanticVersion version = SemanticVersion.parseVersion("v1.2.3"); + assertThat(version.toString(), Matchers.equalTo("v1.2.3")); + } + + @Test + public void testSemanticVersionWithPreRelease() { + SemanticVersion version = SemanticVersion.parseVersion("1.2.3-alpha"); + assertThat(version.toString(), Matchers.equalTo("1.2.3-alpha")); + } + + @Test + public void testSemanticVersionWithBuild() { + SemanticVersion version = SemanticVersion.parseVersion("1.2.3+1234"); + assertThat(version.toString(), Matchers.equalTo("1.2.3+1234")); + } + + @Test + public void testSemanticVersionWithPreReleaseBuild() { + SemanticVersion version = SemanticVersion.parseVersion("1.2.3-alpha+1234"); + assertThat(version.toString(), Matchers.equalTo("1.2.3-alpha+1234")); + } + + @Test + public void testSemanticVersionWithPrefixPreReleaseBuild() { + SemanticVersion version = SemanticVersion.parseVersion("v1.2.3-alpha+1234"); + assertThat(version.toString(), Matchers.equalTo("v1.2.3-alpha+1234")); + } + + @Test + public void testCompareTo() { + assertThat(SemanticVersion.parseVersion("v1.0.0").compareTo( + SemanticVersion.parseVersion("v2.0.0")), + Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("v2.0.0").compareTo( + SemanticVersion.parseVersion("v2.1.0")), + Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("v2.1.0").compareTo( + SemanticVersion.parseVersion("v2.1.1")), + Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("v1.2.3").compareTo( + SemanticVersion.parseVersion("v1.2.3")), + Matchers.equalTo(0)); + + // Build metadata is ignored + assertThat(SemanticVersion.parseVersion("v1.2.3-alpha+102030").compareTo( + SemanticVersion.parseVersion("v1.2.3-alpha+405060")), + Matchers.equalTo(0)); + + // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 + assertThat(SemanticVersion.parseVersion("1.0.0-alpha").compareTo( + SemanticVersion.parseVersion("1.0.0-alpha.1")), Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-alpha.1").compareTo( + SemanticVersion.parseVersion("1.0.0-alpha.beta")), Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-alpha.beta").compareTo( + SemanticVersion.parseVersion("1.0.0-beta")), Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-beta").compareTo( + SemanticVersion.parseVersion("1.0.0-beta.2")), Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-beta.2").compareTo( + SemanticVersion.parseVersion("1.0.0-beta.11")), Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-beta.11").compareTo( + SemanticVersion.parseVersion("1.0.0-rc.1")), Matchers.lessThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-rc.1").compareTo( + SemanticVersion.parseVersion("1.0.0")), Matchers.lessThan(0)); + + // Reversed direction + assertThat(SemanticVersion.parseVersion("1.0.0-alpha.1").compareTo( + SemanticVersion.parseVersion("1.0.0-alpha")), Matchers.greaterThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-beta").compareTo( + SemanticVersion.parseVersion("1.0.0-alpha.alpha.1")), Matchers.greaterThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-beta").compareTo( + SemanticVersion.parseVersion("1.0.0-alpha.beta")), Matchers.greaterThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-beta.2").compareTo( + SemanticVersion.parseVersion("1.0.0-beta")), Matchers.greaterThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-beta.11").compareTo( + SemanticVersion.parseVersion("1.0.0-beta.2")), Matchers.greaterThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0-rc.1").compareTo( + SemanticVersion.parseVersion("1.0.0-beta.11")), Matchers.greaterThan(0)); + + assertThat(SemanticVersion.parseVersion("1.0.0").compareTo( + SemanticVersion.parseVersion("1.0.0-rc.1")), Matchers.greaterThan(0)); + } + + @Test + public void testCompareToWithGoPseudoVersions() { + assertThat(SemanticVersion.parseVersion("v1.2.3-20200518203908-8018eb2c26ba").compareTo( + SemanticVersion.parseVersion("v1.2.3-20191204190536-9bdfabe68543")), Matchers.greaterThan(0)); + } +}