From b7eee33a5b162b4057d04d28f45738e3048bf01d Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 24 Feb 2022 16:44:13 -0500 Subject: Moved SemanticVersionNumber to sevenUnits.utils --- src/main/java/sevenUnits/ProgramInfo.java | 2 + .../java/sevenUnits/SemanticVersionNumber.java | 691 --------------------- .../sevenUnits/utils/SemanticVersionNumber.java | 691 +++++++++++++++++++++ src/test/java/sevenUnits/SemanticVersionTest.java | 399 ------------ .../java/sevenUnits/utils/SemanticVersionTest.java | 399 ++++++++++++ src/test/java/sevenUnitsGUI/package-info.java | 23 - 6 files changed, 1092 insertions(+), 1113 deletions(-) delete mode 100644 src/main/java/sevenUnits/SemanticVersionNumber.java create mode 100644 src/main/java/sevenUnits/utils/SemanticVersionNumber.java delete mode 100644 src/test/java/sevenUnits/SemanticVersionTest.java create mode 100644 src/test/java/sevenUnits/utils/SemanticVersionTest.java delete mode 100644 src/test/java/sevenUnitsGUI/package-info.java (limited to 'src') diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 876367d..6407d7c 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -16,6 +16,8 @@ */ package sevenUnits; +import sevenUnits.utils.SemanticVersionNumber; + /** * Information about 7Units * diff --git a/src/main/java/sevenUnits/SemanticVersionNumber.java b/src/main/java/sevenUnits/SemanticVersionNumber.java deleted file mode 100644 index 01aeb27..0000000 --- a/src/main/java/sevenUnits/SemanticVersionNumber.java +++ /dev/null @@ -1,691 +0,0 @@ -/** - * Copyright (C) 2022 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package sevenUnits; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A version number in the Semantic Versioning - * scheme - *

- * Each version number has three main parts: - *

    - *
  1. The major version, which increments when backwards incompatible changes - * are made - *
  2. The minor version, which increments when backwards compatible feature - * changes are made - *
  3. The patch version, which increments when backwards compatible bug fixes - * are made - *
- * - * @since 2022-02-19 - */ -public final class SemanticVersionNumber - implements Comparable { - /** - * A builder that can be used to create complex version numbers. - *

- * Note: None of this builder's methods tolerate null arguments, arrays - * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls - * throw NullPointerExceptions, everything else throws - * IllegalArgumentException. - * - * @since 2022-02-19 - */ - public static final class Builder { - private final int major; - private final int minor; - private final int patch; - private final List preReleaseIdentifiers; - private final List buildMetadata; - - /** - * Creates a builder which can be used to create a - * {@code SemanticVersionNumber} - * - * @param major major version number of final version - * @param minor minor version number of final version - * @param patch patch version number of final version - * @since 2022-02-19 - */ - private Builder(int major, int minor, int patch) { - this.major = major; - this.minor = minor; - this.patch = patch; - this.preReleaseIdentifiers = new ArrayList<>(); - this.buildMetadata = new ArrayList<>(); - } - - /** - * @return version number created by this builder - * @since 2022-02-19 - */ - public SemanticVersionNumber build() { - return new SemanticVersionNumber(this.major, this.minor, this.patch, - this.preReleaseIdentifiers, this.buildMetadata); - } - - /** - * Adds one or more build metadata identifiers - * - * @param identifiers build metadata - * @return this builder - * @since 2022-02-19 - */ - public Builder buildMetadata(List identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final String identifier : identifiers) { - Objects.requireNonNull(identifier, "identifier may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier)); - this.buildMetadata.add(identifier); - } - return this; - } - - /** - * Adds one or more build metadata identifiers - * - * @param identifiers build metadata - * @return this builder - * @since 2022-02-19 - */ - public Builder buildMetadata(String... identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final String identifier : identifiers) { - Objects.requireNonNull(identifier, "identifier may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier)); - this.buildMetadata.add(identifier); - } - return this; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof Builder)) - return false; - final Builder other = (Builder) obj; - return Objects.equals(this.buildMetadata, other.buildMetadata) - && this.major == other.major && this.minor == other.minor - && this.patch == other.patch && Objects.equals( - this.preReleaseIdentifiers, other.preReleaseIdentifiers); - } - - @Override - public int hashCode() { - return Objects.hash(this.buildMetadata, this.major, this.minor, - this.patch, this.preReleaseIdentifiers); - } - - /** - * Adds one or more numeric identifiers to the version number - * - * @param identifiers pre-release identifier(s) to add - * @return this builder - * @since 2022-02-19 - */ - public Builder preRelease(int... identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final int identifier : identifiers) { - if (identifier < 0) - throw new IllegalArgumentException( - "Numeric identifiers may not be negative"); - this.preReleaseIdentifiers.add(Integer.toString(identifier)); - } - return this; - } - - /** - * Adds one or more pre-release identifier(s) to the version number - * - * @param identifiers pre-release identifier(s) to add - * @return this builder - * @since 2022-02-19 - */ - public Builder preRelease(List identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final String identifier : identifiers) { - Objects.requireNonNull(identifier, "identifier may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier)); - this.preReleaseIdentifiers.add(identifier); - } - return this; - } - - /** - * Adds one or more pre-release identifier(s) to the version number - * - * @param identifiers pre-release identifier(s) to add - * @return this builder - * @since 2022-02-19 - */ - public Builder preRelease(String... identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final String identifier : identifiers) { - Objects.requireNonNull(identifier, "identifier may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier)); - this.preReleaseIdentifiers.add(identifier); - } - return this; - } - - /** - * Adds a string identifier and an integer identifer to pre-release data - * - * @param identifier1 first identifier - * @param identifier2 second identifier - * @return this builder - * @since 2022-02-19 - */ - public Builder preRelease(String identifier1, int identifier2) { - Objects.requireNonNull(identifier1, "identifier1 may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier1).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier1)); - if (identifier2 < 0) - throw new IllegalArgumentException( - "Integer identifier cannot be negative"); - this.preReleaseIdentifiers.add(identifier1); - this.preReleaseIdentifiers.add(Integer.toString(identifier2)); - return this; - } - - @Override - public String toString() { - return "Semantic Version Builder: " + this.build().toString(); - } - } - - /** - * An alternative comparison method for version numbers. This uses the - * version's natural order, but the build metadata will be compared (using - * the same rules as pre-release identifiers) if everything else is equal. - *

- * This ordering is consistent with equals, unlike - * {@code SemanticVersionNumber}'s natural ordering. - */ - public static final Comparator BUILD_METADATA_COMPARATOR = new Comparator<>() { - @Override - public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) { - Objects.requireNonNull(o1, "o1 may not be null"); - Objects.requireNonNull(o2, "o2 may not be null"); - final int naturalComparison = o1.compareTo(o2); - if (naturalComparison == 0) - return SemanticVersionNumber.compare(o1.buildMetadata, - o2.buildMetadata); - else - return naturalComparison; - }; - }; - - /** The alphanumeric pattern all identifiers must follow */ - private static final Pattern VALID_IDENTIFIER = Pattern - .compile("[0-9A-Za-z-]+"); - - /** The numeric pattern which causes special behaviour */ - private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+"); - - /** The pattern for a version number */ - private static final Pattern VERSION_NUMBER = Pattern - .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main - // version - + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release - + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data - - /** - * Creates a builder that can be used to create a version number - * - * @param major major version number of final version - * @param minor minor version number of final version - * @param patch patch version number of final version - * @return version number builder - * @throws IllegalArgumentException if any argument is negative - * @since 2022-02-19 - */ - public static final SemanticVersionNumber.Builder builder(int major, - int minor, int patch) { - if (major < 0) - throw new IllegalArgumentException( - "Major version must be non-negative."); - if (minor < 0) - throw new IllegalArgumentException( - "Minor version must be non-negative."); - if (patch < 0) - throw new IllegalArgumentException( - "Patch version must be non-negative."); - return new SemanticVersionNumber.Builder(major, minor, patch); - } - - /** - * Compares two lists of strings based on SemVer's precedence rules - * - * @param a first list - * @param b second list - * @return result of comparison as in a comparator - * @see Comparator - * @since 2022-02-20 - */ - private static final int compare(List a, List b) { - // test pre-release size - final int aSize = a.size(); - final int bSize = b.size(); - - // no identifiers is greater than any identifiers - if (aSize != 0 && bSize == 0) - return -1; - else if (aSize == 0 && bSize != 0) - return 1; - - // test identifiers one by one - for (int i = 0; i < Math.min(aSize, bSize); i++) { - final String aElement = a.get(i); - final String bElement = b.get(i); - - if (NUMERIC_IDENTIFER.matcher(aElement).matches()) { - if (NUMERIC_IDENTIFER.matcher(bElement).matches()) { - // both are numbers, compare them - final int aNumber = Integer.parseInt(aElement); - final int bNumber = Integer.parseInt(bElement); - - if (aNumber < bNumber) - return -1; - else if (aNumber > bNumber) - return 1; - } else - // aElement is a number and bElement is not a number - // by the rules, a goes before b - return -1; - } else { - if (NUMERIC_IDENTIFER.matcher(bElement).matches()) - // aElement is not a number but bElement is - // by the rules, a goes after b - return 1; - else { - // both are not numbers, compare them - final int comparison = aElement.compareTo(bElement); - if (comparison != 0) - return comparison; - } - } - } - - // we just tested the stuff that's in common, maybe someone has more - if (aSize < bSize) - return -1; - else if (aSize > bSize) - return 1; - else - return 0; - } - - /** - * Gets a version number from a string in the official format - * - * @param versionString string to parse - * @return {@code SemanticVersionNumber} instance - * @since 2022-02-19 - * @see {@link #toString} - */ - public static final SemanticVersionNumber fromString(String versionString) { - // parse & validate version string - Objects.requireNonNull(versionString, "versionString may not be null"); - final Matcher m = VERSION_NUMBER.matcher(versionString); - if (!m.matches()) - throw new IllegalArgumentException( - String.format("Provided string \"%s\" is not a version number", - versionString)); - - // main parts - final int major = Integer.parseInt(m.group(1)); - final int minor = Integer.parseInt(m.group(2)); - final int patch = Integer.parseInt(m.group(3)); - - // pre release - final List preRelease; - if (m.group(4) == null) { - preRelease = List.of(); - } else { - preRelease = Arrays.asList(m.group(4).split("\\.")); - } - - // build metadata - final List buildMetadata; - if (m.group(5) == null) { - buildMetadata = List.of(); - } else { - buildMetadata = Arrays.asList(m.group(5).split("\\.")); - } - - // return number - return new SemanticVersionNumber(major, minor, patch, preRelease, - buildMetadata); - } - - /** - * Tests whether a string is a valid Semantic Version string - * - * @param versionString string to test - * @return true iff string is valid - * @since 2022-02-19 - */ - public static final boolean isValidVersionString(String versionString) { - return VERSION_NUMBER.matcher(versionString).matches(); - } - - /** - * Creates a simple pre-release version number of the form - * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4). - * - * @param major major version number - * @param minor minor version number - * @param patch patch version number - * @param preReleaseType first pre-release element - * @param preReleaseNumber second pre-release element - * @return {@code SemanticVersionNumber} instance - * @throws IllegalArgumentException if any argument is negative or if the - * preReleaseType is null, empty or not - * alphanumeric (0-9, A-Z, a-z, - only) - * @since 2022-02-19 - */ - public static final SemanticVersionNumber preRelease(int major, int minor, - int patch, String preReleaseType, int preReleaseNumber) { - if (major < 0) - throw new IllegalArgumentException( - "Major version must be non-negative."); - if (minor < 0) - throw new IllegalArgumentException( - "Minor version must be non-negative."); - if (patch < 0) - throw new IllegalArgumentException( - "Patch version must be non-negative."); - Objects.requireNonNull(preReleaseType, "preReleaseType may not be null"); - if (!VALID_IDENTIFIER.matcher(preReleaseType).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\".", preReleaseType)); - if (preReleaseNumber < 0) - throw new IllegalArgumentException( - "Pre-release number must be non-negative."); - return new SemanticVersionNumber(major, minor, patch, - List.of(preReleaseType, Integer.toString(preReleaseNumber)), - List.of()); - } - - /** - * Creates a {@code SemanticVersionNumber} instance without pre-release - * identifiers or build metadata. - *

- * Note: this method allows you to create versions with major version number - * 0, even though these versions would not be considered stable. - * - * @param major major version number - * @param minor minor version number - * @param patch patch version number - * @return {@code SemanticVersionNumber} instance - * @throws IllegalArgumentException if any argument is negative - * @since 2022-02-19 - */ - public static final SemanticVersionNumber stableVersion(int major, int minor, - int patch) { - if (major < 0) - throw new IllegalArgumentException( - "Major version must be non-negative."); - if (minor < 0) - throw new IllegalArgumentException( - "Minor version must be non-negative."); - if (patch < 0) - throw new IllegalArgumentException( - "Patch version must be non-negative."); - return new SemanticVersionNumber(major, minor, patch, List.of(), - List.of()); - } - - // parts of the version number - private final int major; - private final int minor; - private final int patch; - private final List preReleaseIdentifiers; - private final List buildMetadata; - - /** - * Creates a version number - * - * @param major major version number - * @param minor minor version number - * @param patch patch version number - * @param preReleaseIdentifiers pre-release version data - * @param buildMetadata build metadata - * @since 2022-02-19 - */ - private SemanticVersionNumber(int major, int minor, int patch, - List preReleaseIdentifiers, List buildMetadata) { - this.major = major; - this.minor = minor; - this.patch = patch; - this.preReleaseIdentifiers = preReleaseIdentifiers; - this.buildMetadata = buildMetadata; - } - - /** - * @return build metadata (empty if there is none) - * @since 2022-02-19 - */ - public List buildMetadata() { - return Collections.unmodifiableList(this.buildMetadata); - } - - /** - * Compares two version numbers according to the official Semantic Versioning - * order. - *

- * Note: this ordering is not consistent with equals. Specifically, two - * versions that are identical except for their build metadata will be - * considered different by equals but the same by this method. This is - * required to follow the official Semantic Versioning specification. - *

- */ - @Override - public int compareTo(SemanticVersionNumber o) { - // test the three big numbers in order first - if (this.major < o.major) - return -1; - else if (this.major > o.major) - return 1; - - if (this.minor < o.minor) - return -1; - else if (this.minor > o.minor) - return 1; - - if (this.patch < o.patch) - return -1; - else if (this.patch > o.patch) - return 1; - - // now we just compare pre-release identifiers - // (remember: build metadata is ignored) - return SemanticVersionNumber.compare(this.preReleaseIdentifiers, - o.preReleaseIdentifiers); - } - - /** - * Determines the compatibility of code written for this version to - * {@code other}. More specifically: - *

- * If this function returns true, then there should be no problems - * upgrading code written for this version to version {@code other} as long - * as: - *

    - *
  • Semantic Versioning is being used properly - *
  • Your code doesn't depend on unintended features (if it does, it isn't - * necessarily compatible with any other version) - *
- * If this function returns false, you may have to change your code to - * upgrade it to {@code other} - * - *

- * Two version numbers that are identical (ignoring build metadata) are - * always compatible. Different version numbers are compatible as long as: - *

    - *
  • The major version number is not 0 (if it is, the API is considered - * unstable and any upgrade can be backwards compatible) - *
  • The major version number is the same (changing the major version - * number implies bacwards incompatible changes) - *
  • This version comes before the other one in the official precedence - * order (downgrading can remove features you depend on) - *
- * - * @param other version to compare with - * @return true if you can definitely upgrade to {@code other} without - * changing code - * @since 2022-02-20 - */ - public boolean compatibleWith(SemanticVersionNumber other) { - Objects.requireNonNull(other, "other may not be null"); - - return this.compareTo(other) == 0 || this.major != 0 - && this.major == other.major && this.compareTo(other) < 0; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof SemanticVersionNumber)) - return false; - final SemanticVersionNumber other = (SemanticVersionNumber) obj; - if (this.buildMetadata == null) { - if (other.buildMetadata != null) - return false; - } else if (!this.buildMetadata.equals(other.buildMetadata)) - return false; - if (this.major != other.major) - return false; - if (this.minor != other.minor) - return false; - if (this.patch != other.patch) - return false; - if (this.preReleaseIdentifiers == null) { - if (other.preReleaseIdentifiers != null) - return false; - } else if (!this.preReleaseIdentifiers - .equals(other.preReleaseIdentifiers)) - return false; - return true; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result - + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode()); - result = prime * result + this.major; - result = prime * result + this.minor; - result = prime * result + this.patch; - result = prime * result + (this.preReleaseIdentifiers == null ? 0 - : this.preReleaseIdentifiers.hashCode()); - return result; - } - - /** - * @return true iff this version is stable (major version > 0 and not a - * pre-release) - * @since 2022-02-19 - */ - public boolean isStable() { - return this.major > 0 && this.preReleaseIdentifiers.isEmpty(); - } - - /** - * @return the MAJOR version number, incremented when you make backwards - * incompatible API changes - * @since 2022-02-19 - */ - public int majorVersion() { - return this.major; - } - - /** - * @return the MINOR version number, incremented when you add backwards - * compatible functionality - * @since 2022-02-19 - */ - public int minorVersion() { - return this.minor; - } - - /** - * @return the PATCH version number, incremented when you make backwards - * compatible bug fixes - * @since 2022-02-19 - */ - public int patchVersion() { - return this.patch; - } - - /** - * @return identifiers describing this pre-release (empty if not a - * pre-release) - * @since 2022-02-19 - */ - public List preReleaseIdentifiers() { - return Collections.unmodifiableList(this.preReleaseIdentifiers); - } - - /** - * Converts a version number to a string using the official SemVer format. - * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If - * pre-release identifiers are present, they are separated by periods and - * added after a '-'. If build metadata is present, it is separated by - * periods and added after a '+'. Pre-release identifiers go before version - * metadata. - *

- * For example, the version with major number 3, minor number 2, patch number - * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19" - * has a string representation "3.2.1-alpha.1+2022-02-19". - * - * @see The official SemVer specification - */ - @Override - public String toString() { - String versionString = String.format("%d.%d.%d", this.major, this.minor, - this.patch); - if (!this.preReleaseIdentifiers.isEmpty()) { - versionString += "-" + String.join(".", this.preReleaseIdentifiers); - } - if (!this.buildMetadata.isEmpty()) { - versionString += "+" + String.join(".", this.buildMetadata); - } - return versionString; - } -} diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java new file mode 100644 index 0000000..06417c5 --- /dev/null +++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java @@ -0,0 +1,691 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnits.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A version number in the Semantic Versioning + * scheme + *

+ * Each version number has three main parts: + *

    + *
  1. The major version, which increments when backwards incompatible changes + * are made + *
  2. The minor version, which increments when backwards compatible feature + * changes are made + *
  3. The patch version, which increments when backwards compatible bug fixes + * are made + *
+ * + * @since 2022-02-19 + */ +public final class SemanticVersionNumber + implements Comparable { + /** + * A builder that can be used to create complex version numbers. + *

+ * Note: None of this builder's methods tolerate null arguments, arrays + * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls + * throw NullPointerExceptions, everything else throws + * IllegalArgumentException. + * + * @since 2022-02-19 + */ + public static final class Builder { + private final int major; + private final int minor; + private final int patch; + private final List preReleaseIdentifiers; + private final List buildMetadata; + + /** + * Creates a builder which can be used to create a + * {@code SemanticVersionNumber} + * + * @param major major version number of final version + * @param minor minor version number of final version + * @param patch patch version number of final version + * @since 2022-02-19 + */ + private Builder(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseIdentifiers = new ArrayList<>(); + this.buildMetadata = new ArrayList<>(); + } + + /** + * @return version number created by this builder + * @since 2022-02-19 + */ + public SemanticVersionNumber build() { + return new SemanticVersionNumber(this.major, this.minor, this.patch, + this.preReleaseIdentifiers, this.buildMetadata); + } + + /** + * Adds one or more build metadata identifiers + * + * @param identifiers build metadata + * @return this builder + * @since 2022-02-19 + */ + public Builder buildMetadata(List identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.buildMetadata.add(identifier); + } + return this; + } + + /** + * Adds one or more build metadata identifiers + * + * @param identifiers build metadata + * @return this builder + * @since 2022-02-19 + */ + public Builder buildMetadata(String... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.buildMetadata.add(identifier); + } + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof Builder)) + return false; + final Builder other = (Builder) obj; + return Objects.equals(this.buildMetadata, other.buildMetadata) + && this.major == other.major && this.minor == other.minor + && this.patch == other.patch && Objects.equals( + this.preReleaseIdentifiers, other.preReleaseIdentifiers); + } + + @Override + public int hashCode() { + return Objects.hash(this.buildMetadata, this.major, this.minor, + this.patch, this.preReleaseIdentifiers); + } + + /** + * Adds one or more numeric identifiers to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(int... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final int identifier : identifiers) { + if (identifier < 0) + throw new IllegalArgumentException( + "Numeric identifiers may not be negative"); + this.preReleaseIdentifiers.add(Integer.toString(identifier)); + } + return this; + } + + /** + * Adds one or more pre-release identifier(s) to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(List identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.preReleaseIdentifiers.add(identifier); + } + return this; + } + + /** + * Adds one or more pre-release identifier(s) to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(String... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.preReleaseIdentifiers.add(identifier); + } + return this; + } + + /** + * Adds a string identifier and an integer identifer to pre-release data + * + * @param identifier1 first identifier + * @param identifier2 second identifier + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(String identifier1, int identifier2) { + Objects.requireNonNull(identifier1, "identifier1 may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier1).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier1)); + if (identifier2 < 0) + throw new IllegalArgumentException( + "Integer identifier cannot be negative"); + this.preReleaseIdentifiers.add(identifier1); + this.preReleaseIdentifiers.add(Integer.toString(identifier2)); + return this; + } + + @Override + public String toString() { + return "Semantic Version Builder: " + this.build().toString(); + } + } + + /** + * An alternative comparison method for version numbers. This uses the + * version's natural order, but the build metadata will be compared (using + * the same rules as pre-release identifiers) if everything else is equal. + *

+ * This ordering is consistent with equals, unlike + * {@code SemanticVersionNumber}'s natural ordering. + */ + public static final Comparator BUILD_METADATA_COMPARATOR = new Comparator<>() { + @Override + public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) { + Objects.requireNonNull(o1, "o1 may not be null"); + Objects.requireNonNull(o2, "o2 may not be null"); + final int naturalComparison = o1.compareTo(o2); + if (naturalComparison == 0) + return SemanticVersionNumber.compareIdentifiers(o1.buildMetadata, + o2.buildMetadata); + else + return naturalComparison; + }; + }; + + /** The alphanumeric pattern all identifiers must follow */ + private static final Pattern VALID_IDENTIFIER = Pattern + .compile("[0-9A-Za-z-]+"); + + /** The numeric pattern which causes special behaviour */ + private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+"); + + /** The pattern for a version number */ + private static final Pattern VERSION_NUMBER = Pattern + .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main + // version + + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release + + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data + + /** + * Creates a builder that can be used to create a version number + * + * @param major major version number of final version + * @param minor minor version number of final version + * @param patch patch version number of final version + * @return version number builder + * @throws IllegalArgumentException if any argument is negative + * @since 2022-02-19 + */ + public static final SemanticVersionNumber.Builder builder(int major, + int minor, int patch) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + return new SemanticVersionNumber.Builder(major, minor, patch); + } + + /** + * Compares two lists of strings based on SemVer's precedence rules + * + * @param a first list + * @param b second list + * @return result of comparison as in a comparator + * @see Comparator + * @since 2022-02-20 + */ + private static final int compareIdentifiers(List a, List b) { + // test pre-release size + final int aSize = a.size(); + final int bSize = b.size(); + + // no identifiers is greater than any identifiers + if (aSize != 0 && bSize == 0) + return -1; + else if (aSize == 0 && bSize != 0) + return 1; + + // test identifiers one by one + for (int i = 0; i < Math.min(aSize, bSize); i++) { + final String aElement = a.get(i); + final String bElement = b.get(i); + + if (NUMERIC_IDENTIFER.matcher(aElement).matches()) { + if (NUMERIC_IDENTIFER.matcher(bElement).matches()) { + // both are numbers, compare them + final int aNumber = Integer.parseInt(aElement); + final int bNumber = Integer.parseInt(bElement); + + if (aNumber < bNumber) + return -1; + else if (aNumber > bNumber) + return 1; + } else + // aElement is a number and bElement is not a number + // by the rules, a goes before b + return -1; + } else { + if (NUMERIC_IDENTIFER.matcher(bElement).matches()) + // aElement is not a number but bElement is + // by the rules, a goes after b + return 1; + else { + // both are not numbers, compare them + final int comparison = aElement.compareTo(bElement); + if (comparison != 0) + return comparison; + } + } + } + + // we just tested the stuff that's in common, maybe someone has more + if (aSize < bSize) + return -1; + else if (aSize > bSize) + return 1; + else + return 0; + } + + /** + * Gets a version number from a string in the official format + * + * @param versionString string to parse + * @return {@code SemanticVersionNumber} instance + * @since 2022-02-19 + * @see {@link #toString} + */ + public static final SemanticVersionNumber fromString(String versionString) { + // parse & validate version string + Objects.requireNonNull(versionString, "versionString may not be null"); + final Matcher m = VERSION_NUMBER.matcher(versionString); + if (!m.matches()) + throw new IllegalArgumentException( + String.format("Provided string \"%s\" is not a version number", + versionString)); + + // main parts + final int major = Integer.parseInt(m.group(1)); + final int minor = Integer.parseInt(m.group(2)); + final int patch = Integer.parseInt(m.group(3)); + + // pre release + final List preRelease; + if (m.group(4) == null) { + preRelease = List.of(); + } else { + preRelease = Arrays.asList(m.group(4).split("\\.")); + } + + // build metadata + final List buildMetadata; + if (m.group(5) == null) { + buildMetadata = List.of(); + } else { + buildMetadata = Arrays.asList(m.group(5).split("\\.")); + } + + // return number + return new SemanticVersionNumber(major, minor, patch, preRelease, + buildMetadata); + } + + /** + * Tests whether a string is a valid Semantic Version string + * + * @param versionString string to test + * @return true iff string is valid + * @since 2022-02-19 + */ + public static final boolean isValidVersionString(String versionString) { + return VERSION_NUMBER.matcher(versionString).matches(); + } + + /** + * Creates a simple pre-release version number of the form + * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4). + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @param preReleaseType first pre-release element + * @param preReleaseNumber second pre-release element + * @return {@code SemanticVersionNumber} instance + * @throws IllegalArgumentException if any argument is negative or if the + * preReleaseType is null, empty or not + * alphanumeric (0-9, A-Z, a-z, - only) + * @since 2022-02-19 + */ + public static final SemanticVersionNumber preRelease(int major, int minor, + int patch, String preReleaseType, int preReleaseNumber) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + Objects.requireNonNull(preReleaseType, "preReleaseType may not be null"); + if (!VALID_IDENTIFIER.matcher(preReleaseType).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\".", preReleaseType)); + if (preReleaseNumber < 0) + throw new IllegalArgumentException( + "Pre-release number must be non-negative."); + return new SemanticVersionNumber(major, minor, patch, + List.of(preReleaseType, Integer.toString(preReleaseNumber)), + List.of()); + } + + /** + * Creates a {@code SemanticVersionNumber} instance without pre-release + * identifiers or build metadata. + *

+ * Note: this method allows you to create versions with major version number + * 0, even though these versions would not be considered stable. + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @return {@code SemanticVersionNumber} instance + * @throws IllegalArgumentException if any argument is negative + * @since 2022-02-19 + */ + public static final SemanticVersionNumber stableVersion(int major, int minor, + int patch) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + return new SemanticVersionNumber(major, minor, patch, List.of(), + List.of()); + } + + // parts of the version number + private final int major; + private final int minor; + private final int patch; + private final List preReleaseIdentifiers; + private final List buildMetadata; + + /** + * Creates a version number + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @param preReleaseIdentifiers pre-release version data + * @param buildMetadata build metadata + * @since 2022-02-19 + */ + private SemanticVersionNumber(int major, int minor, int patch, + List preReleaseIdentifiers, List buildMetadata) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseIdentifiers = preReleaseIdentifiers; + this.buildMetadata = buildMetadata; + } + + /** + * @return build metadata (empty if there is none) + * @since 2022-02-19 + */ + public List buildMetadata() { + return Collections.unmodifiableList(this.buildMetadata); + } + + /** + * Compares two version numbers according to the official Semantic Versioning + * order. + *

+ * Note: this ordering is not consistent with equals. Specifically, two + * versions that are identical except for their build metadata will be + * considered different by equals but the same by this method. This is + * required to follow the official Semantic Versioning specification. + *

+ */ + @Override + public int compareTo(SemanticVersionNumber o) { + // test the three big numbers in order first + if (this.major < o.major) + return -1; + else if (this.major > o.major) + return 1; + + if (this.minor < o.minor) + return -1; + else if (this.minor > o.minor) + return 1; + + if (this.patch < o.patch) + return -1; + else if (this.patch > o.patch) + return 1; + + // now we just compare pre-release identifiers + // (remember: build metadata is ignored) + return SemanticVersionNumber.compareIdentifiers(this.preReleaseIdentifiers, + o.preReleaseIdentifiers); + } + + /** + * Determines the compatibility of code written for this version to + * {@code other}. More specifically: + *

+ * If this function returns true, then there should be no problems + * upgrading code written for this version to version {@code other} as long + * as: + *

    + *
  • Semantic Versioning is being used properly + *
  • Your code doesn't depend on unintended features (if it does, it isn't + * necessarily compatible with any other version) + *
+ * If this function returns false, you may have to change your code to + * upgrade it to {@code other} + * + *

+ * Two version numbers that are identical (ignoring build metadata) are + * always compatible. Different version numbers are compatible as long as: + *

    + *
  • The major version number is not 0 (if it is, the API is considered + * unstable and any upgrade can be backwards compatible) + *
  • The major version number is the same (changing the major version + * number implies bacwards incompatible changes) + *
  • This version comes before the other one in the official precedence + * order (downgrading can remove features you depend on) + *
+ * + * @param other version to compare with + * @return true if you can definitely upgrade to {@code other} without + * changing code + * @since 2022-02-20 + */ + public boolean compatibleWith(SemanticVersionNumber other) { + Objects.requireNonNull(other, "other may not be null"); + + return this.compareTo(other) == 0 || this.major != 0 + && this.major == other.major && this.compareTo(other) < 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof SemanticVersionNumber)) + return false; + final SemanticVersionNumber other = (SemanticVersionNumber) obj; + if (this.buildMetadata == null) { + if (other.buildMetadata != null) + return false; + } else if (!this.buildMetadata.equals(other.buildMetadata)) + return false; + if (this.major != other.major) + return false; + if (this.minor != other.minor) + return false; + if (this.patch != other.patch) + return false; + if (this.preReleaseIdentifiers == null) { + if (other.preReleaseIdentifiers != null) + return false; + } else if (!this.preReleaseIdentifiers + .equals(other.preReleaseIdentifiers)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode()); + result = prime * result + this.major; + result = prime * result + this.minor; + result = prime * result + this.patch; + result = prime * result + (this.preReleaseIdentifiers == null ? 0 + : this.preReleaseIdentifiers.hashCode()); + return result; + } + + /** + * @return true iff this version is stable (major version > 0 and not a + * pre-release) + * @since 2022-02-19 + */ + public boolean isStable() { + return this.major > 0 && this.preReleaseIdentifiers.isEmpty(); + } + + /** + * @return the MAJOR version number, incremented when you make backwards + * incompatible API changes + * @since 2022-02-19 + */ + public int majorVersion() { + return this.major; + } + + /** + * @return the MINOR version number, incremented when you add backwards + * compatible functionality + * @since 2022-02-19 + */ + public int minorVersion() { + return this.minor; + } + + /** + * @return the PATCH version number, incremented when you make backwards + * compatible bug fixes + * @since 2022-02-19 + */ + public int patchVersion() { + return this.patch; + } + + /** + * @return identifiers describing this pre-release (empty if not a + * pre-release) + * @since 2022-02-19 + */ + public List preReleaseIdentifiers() { + return Collections.unmodifiableList(this.preReleaseIdentifiers); + } + + /** + * Converts a version number to a string using the official SemVer format. + * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If + * pre-release identifiers are present, they are separated by periods and + * added after a '-'. If build metadata is present, it is separated by + * periods and added after a '+'. Pre-release identifiers go before version + * metadata. + *

+ * For example, the version with major number 3, minor number 2, patch number + * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19" + * has a string representation "3.2.1-alpha.1+2022-02-19". + * + * @see The official SemVer specification + */ + @Override + public String toString() { + String versionString = String.format("%d.%d.%d", this.major, this.minor, + this.patch); + if (!this.preReleaseIdentifiers.isEmpty()) { + versionString += "-" + String.join(".", this.preReleaseIdentifiers); + } + if (!this.buildMetadata.isEmpty()) { + versionString += "+" + String.join(".", this.buildMetadata); + } + return versionString; + } +} diff --git a/src/test/java/sevenUnits/SemanticVersionTest.java b/src/test/java/sevenUnits/SemanticVersionTest.java deleted file mode 100644 index 9202ef9..0000000 --- a/src/test/java/sevenUnits/SemanticVersionTest.java +++ /dev/null @@ -1,399 +0,0 @@ -/** - * Copyright (C) 2022 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package sevenUnits; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static sevenUnits.SemanticVersionNumber.BUILD_METADATA_COMPARATOR; -import static sevenUnits.SemanticVersionNumber.builder; -import static sevenUnits.SemanticVersionNumber.fromString; -import static sevenUnits.SemanticVersionNumber.isValidVersionString; -import static sevenUnits.SemanticVersionNumber.preRelease; -import static sevenUnits.SemanticVersionNumber.stableVersion; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link SemanticVersionNumber} - * - * @since 2022-02-19 - */ -public final class SemanticVersionTest { - /** - * Test for {@link SemanticVersionNumber#compatible} - * - * @since 2022-02-20 - */ - @Test - public void testCompatibility() { - assertTrue(stableVersion(1, 0, 0).compatibleWith(stableVersion(1, 0, 5)), - "1.0.0 not compatible with 1.0.5"); - assertTrue(stableVersion(1, 3, 1).compatibleWith(stableVersion(1, 4, 0)), - "1.3.1 not compatible with 1.4.0"); - - // 0.y.z should not be compatible with any other version - assertFalse(stableVersion(0, 4, 0).compatibleWith(stableVersion(0, 4, 1)), - "0.4.0 compatible with 0.4.1 (0.y.z versions should be treated as unstable/incompatbile)"); - - // upgrading major version should = incompatible - assertFalse(stableVersion(1, 0, 0).compatibleWith(stableVersion(2, 0, 0)), - "1.0.0 compatible with 2.0.0"); - - // dowgrade should = incompatible - assertFalse(stableVersion(1, 1, 0).compatibleWith(stableVersion(1, 0, 0)), - "1.1.0 compatible with 1.0.0"); - } - - /** - * Tests {@link SemanticVersionNumber#toString} for complex version numbers - * - * @since 2022-02-19 - */ - @Test - public void testComplexToString() { - final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) - .build(); - assertEquals("1.2.3-1.2.3", v1.toString()); - final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) - .buildMetadata("2022-02-19").build(); - assertEquals("4.5.6-abc.123+2022-02-19", v2.toString()); - final SemanticVersionNumber v3 = builder(1, 0, 0) - .preRelease("x-y-z", "--").build(); - assertEquals("1.0.0-x-y-z.--", v3.toString()); - } - - /** - * Tests that complex version can be created and their parts read - * - * @since 2022-02-19 - */ - @Test - public void testComplexVersions() { - final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) - .build(); - assertEquals(1, v1.majorVersion()); - assertEquals(2, v1.minorVersion()); - assertEquals(3, v1.patchVersion()); - assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers()); - assertEquals(List.of(), v1.buildMetadata()); - - final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) - .buildMetadata("2022-02-19").build(); - assertEquals(4, v2.majorVersion()); - assertEquals(5, v2.minorVersion()); - assertEquals(6, v2.patchVersion()); - assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers()); - assertEquals(List.of("2022-02-19"), v2.buildMetadata()); - - final SemanticVersionNumber v3 = builder(1, 0, 0) - .preRelease("x-y-z", "--").build(); - assertEquals(1, v3.majorVersion()); - assertEquals(0, v3.minorVersion()); - assertEquals(0, v3.patchVersion()); - assertEquals(List.of("x-y-z", "--"), v3.preReleaseIdentifiers()); - assertEquals(List.of(), v3.buildMetadata()); - } - - /** - * Test that semantic version strings can be parsed correctly - * - * @since 2022-02-19 - * @see SemanticVersionNumber#fromString - * @see SemanticVersionNumber#isValidVersionString - */ - @Test - public void testFromString() { - // test that the regex can match version strings - assertTrue(isValidVersionString("1.0.0"), "1.0.0 is treated as invalid"); - assertTrue(isValidVersionString("1.3.9"), "1.3.9 is treated as invalid"); - assertTrue(isValidVersionString("2.0.0-a.1"), - "2.0.0-a.1 is treated as invalid"); - assertTrue(isValidVersionString("1.0.0-a.b.c.d"), - "1.0.0-a.b.c.d is treated as invalid"); - assertTrue(isValidVersionString("1.0.0+abc"), - "1.0.0+abc is treated as invalid"); - assertTrue(isValidVersionString("1.0.0-abc+def"), - "1.0.0-abc+def is treated as invalid"); - - // test that invalid versions don't match - assertFalse(isValidVersionString("1.0"), - "1.0 is treated as valid (patch should be required)"); - assertFalse(isValidVersionString("1.A.0"), - "1.A.0 is treated as valid (main versions must be numbers)"); - assertFalse(isValidVersionString("1.0.0-"), - "1.0.0- is treated as valid (pre-release must not be empty)"); - assertFalse(isValidVersionString("1.0.0+"), - "1.0.0+ is treated as valid (build metadata must not be empty)"); - - // test that versions can be parsed - assertEquals(stableVersion(1, 0, 0), fromString("1.0.0"), - "Could not parse 1.0.0"); - assertEquals( - builder(1, 2, 3).preRelease("abc", "56", "def") - .buildMetadata("2022abc99").build(), - fromString("1.2.3-abc.56.def+2022abc99"), - "Could not parse 1.2.3-abc.56.def+2022abc99"); - } - - /** - * Ensures it is impossible to create invalid version numbers - */ - @Test - public void testInvalidVersionNumbers() { - // stableVersion() - assertThrows(IllegalArgumentException.class, - () -> stableVersion(1, 0, -1), - "Negative patch tolerated by stableVersion"); - assertThrows(IllegalArgumentException.class, - () -> stableVersion(1, -2, 1), - "Negative minor version number tolerated by stableVersion"); - assertThrows(IllegalArgumentException.class, - () -> stableVersion(-3, 0, 7), - "Negative major version number tolerated by stableVersion"); - - // preRelease() - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, 0, -1, "test", 2), - "Negative patch tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, -2, 1, "test", 2), - "Negative minor version number tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(-3, 0, 7, "test", 2), - "Negative major version number tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, 0, 0, "test", -1), - "Negative pre release number tolerated by preRelease"); - assertThrows(NullPointerException.class, - () -> preRelease(1, 0, 0, null, 1), "Null tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, 0, 0, "", 1), - "Empty string tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, 0, 0, "abc+cde", 1), - "Invalid string tolerated by preRelease"); - - // builder() - assertThrows(IllegalArgumentException.class, () -> builder(1, 0, -1), - "Negative patch tolerated by builder"); - assertThrows(IllegalArgumentException.class, () -> builder(1, -2, 1), - "Negative minor version number tolerated by builder"); - assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7), - "Negative major version number tolerated by builder"); - - final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3); - // note: builder.buildMetadata(null) doesn't even compile lol - // builder.buildMetadata - assertThrows(NullPointerException.class, - () -> testBuilder.buildMetadata(null, "abc"), - "Null tolerated by builder.buildMetadata(String...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata(""), - "Empty string tolerated by builder.buildMetadata(String...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata("c%4"), - "Invalid string tolerated by builder.buildMetadata(String...)"); - assertThrows(NullPointerException.class, - () -> testBuilder.buildMetadata(List.of("abc", null)), - "Null tolerated by builder.buildMetadata(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata(List.of("")), - "Empty string tolerated by builder.buildMetadata(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata(List.of("")), - "Invalid string tolerated by builder.buildMetadata(List)"); - - // builder.preRelease - assertThrows(NullPointerException.class, - () -> testBuilder.preRelease(null, "abc"), - "Null tolerated by builder.preRelease(String...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(""), - "Empty string tolerated by builder.preRelease(String...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease("c%4"), - "Invalid string tolerated by builder.preRelease(String...)"); - assertThrows(NullPointerException.class, - () -> testBuilder.preRelease(List.of("abc", null)), - "Null tolerated by builder.preRelease(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(List.of("")), - "Empty string tolerated by builder.preRelease(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(List.of("")), - "Invalid string tolerated by builder.preRelease(List)"); - - // the overloadings that accept numeric arguments - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(-1), - "Negative number tolerated by builder.preRelease(int...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease("abc", -1), - "Negative number tolerated by builder.preRelease(String, int)"); - assertThrows(NullPointerException.class, - () -> testBuilder.preRelease(null, 1), - "Null tolerated by builder.preRelease(String, int)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease("", 1), - "Empty string tolerated by builder.preRelease(String, int)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease("#$#c", 1), - "Invalid string tolerated by builder.preRelease(String, int)"); - - // ensure all these attempts didn't change the builder - assertEquals(builder(1, 2, 3), testBuilder, - "Attempts at making invalid version number succeeded despite throwing errors"); - } - - /** - * Test for {@link SemanticVersionNumber#isStable} - * - * @since 2022-02-19 - */ - @Test - public void testIsStable() { - assertTrue(stableVersion(1, 0, 0).isStable(), - "1.0.0 should be stable but is not"); - assertFalse(stableVersion(0, 1, 2).isStable(), - "0.1.2 should not be stable but is"); - assertFalse(preRelease(1, 2, 3, "alpha", 5).isStable(), - "1.2.3a5 should not be stable but is"); - assertTrue( - builder(9, 9, 99) - .buildMetadata("lots-of-metadata", "abc123", "2022").build() - .isStable(), - "9.9.99+lots-of-metadata.abc123.2022 should be stable but is not"); - } - - /** - * Tests that the versions are ordered by - * {@link SemanticVersionNumber#compareTo} according to official rules. Tests - * all of the versions compared in section 11 of the SemVer 2.0.0 document - * and some more. - * - * @since 2022-02-19 - */ - @Test - public void testOrder() { - final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha") - .build(); // 1.0.0-alpha - final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1 - final SemanticVersionNumber v100ab = builder(1, 0, 0) - .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta - final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta") - .build(); // 1.0.0-alpha - final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2 - final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11 - final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1 - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); - final SemanticVersionNumber v100plus = builder(1, 0, 0) - .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah - final SemanticVersionNumber v200 = stableVersion(2, 0, 0); - final SemanticVersionNumber v201 = stableVersion(2, 0, 1); - final SemanticVersionNumber v210 = stableVersion(2, 1, 0); - final SemanticVersionNumber v211 = stableVersion(2, 1, 1); - final SemanticVersionNumber v300 = stableVersion(3, 0, 0); - - // test order of version numbers - assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1"); - assertTrue(v100a1.compareTo(v100ab) < 0, - "1.0.0-alpha.1 >= 1.0.0-alpha.beta"); - assertTrue(v100ab.compareTo(v100b) < 0, "1.0.0-alpha.beta >= 1.0.0-beta"); - assertTrue(v100b.compareTo(v100b2) < 0, "1.0.0-beta >= 1.0.0-beta.2"); - assertTrue(v100b2.compareTo(v100b11) < 0, - "1.0.0-beta.2 >= 1.0.0-beta.11"); - assertTrue(v100b11.compareTo(v100rc1) < 0, "1.0.0-beta.11 >= 1.0.0-rc.1"); - assertTrue(v100rc1.compareTo(v100) < 0, "1.0.0-rc.1 >= 1.0.0"); - assertTrue(v100.compareTo(v200) < 0, "1.0.0 >= 2.0.0"); - assertTrue(v200.compareTo(v201) < 0, "2.0.0 >= 2.0.1"); - assertTrue(v201.compareTo(v210) < 0, "2.0.1 >= 2.1.0"); - assertTrue(v210.compareTo(v211) < 0, "2.1.0 >= 2.1.1"); - assertTrue(v211.compareTo(v300) < 0, "2.1.1 >= 3.0.0"); - - // test symmetry - assume previous tests passed - assertTrue(v100a1.compareTo(v100a) > 0, "1.0.0-alpha.1 <= 1.0.0-alpha"); - assertTrue(v100.compareTo(v100rc1) > 0, "1.0.0 <= 1.0.0-rc.1"); - assertTrue(v300.compareTo(v211) > 0, "3.0.0 <= 2.1.1"); - - // test transitivity - assertTrue(v100a.compareTo(v100b11) < 0, "1.0.0-alpha >= 1.0.0-beta.11"); - assertTrue(v100b.compareTo(v200) < 0, "1.0.0-beta >= 2.0.0"); - assertTrue(v100.compareTo(v300) < 0, "1.0.0 >= 3.0.0"); - assertTrue(v100a.compareTo(v300) < 0, "1.0.0-alpha >= 3.0.0"); - - // test metadata is ignored - assertEquals(0, v100.compareTo(v100plus), "Build metadata not ignored"); - // test metadata is NOT ignored by alternative comparator - assertTrue(BUILD_METADATA_COMPARATOR.compare(v100, v100plus) > 0, - "Build metadata ignored by BUILD_METADATA_COMPARATOR"); - } - - /** - * Tests that simple stable versions can be created and their parts read - * - * @since 2022-02-19 - */ - @Test - public void testSimpleStableVersions() { - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); - assertEquals(1, v100.majorVersion()); - assertEquals(0, v100.minorVersion()); - assertEquals(0, v100.patchVersion()); - - final SemanticVersionNumber v925 = stableVersion(9, 2, 5); - assertEquals(9, v925.majorVersion()); - assertEquals(2, v925.minorVersion()); - assertEquals(5, v925.patchVersion()); - } - - /** - * Tests that {@link SemanticVersionNumber#toString} works for simple version - * numbers - * - * @since 2022-02-19 - */ - @Test - public void testSimpleToString() { - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); - assertEquals("1.0.0", v100.toString()); - - final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1); - assertEquals("8.4.5-alpha.1", v845a1.toString()); - } - - /** - * Tests that simple unstable versions can be created and their parts read - * - * @since 2022-02-19 - */ - @Test - public void testSimpleUnstableVersions() { - final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1); - assertEquals(3, v350a1.majorVersion(), - "Incorrect major version for v3.5.0a1"); - assertEquals(5, v350a1.minorVersion(), - "Incorrect minor version for v3.5.0a1"); - assertEquals(0, v350a1.patchVersion(), - "Incorrect patch version for v3.5.0a1"); - assertEquals(List.of("alpha", "1"), v350a1.preReleaseIdentifiers(), - "Incorrect pre-release identifiers for v3.5.0a1"); - } -} diff --git a/src/test/java/sevenUnits/utils/SemanticVersionTest.java b/src/test/java/sevenUnits/utils/SemanticVersionTest.java new file mode 100644 index 0000000..877b258 --- /dev/null +++ b/src/test/java/sevenUnits/utils/SemanticVersionTest.java @@ -0,0 +1,399 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnits.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static sevenUnits.utils.SemanticVersionNumber.BUILD_METADATA_COMPARATOR; +import static sevenUnits.utils.SemanticVersionNumber.builder; +import static sevenUnits.utils.SemanticVersionNumber.fromString; +import static sevenUnits.utils.SemanticVersionNumber.isValidVersionString; +import static sevenUnits.utils.SemanticVersionNumber.preRelease; +import static sevenUnits.utils.SemanticVersionNumber.stableVersion; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SemanticVersionNumber} + * + * @since 2022-02-19 + */ +public final class SemanticVersionTest { + /** + * Test for {@link SemanticVersionNumber#compatible} + * + * @since 2022-02-20 + */ + @Test + public void testCompatibility() { + assertTrue(stableVersion(1, 0, 0).compatibleWith(stableVersion(1, 0, 5)), + "1.0.0 not compatible with 1.0.5"); + assertTrue(stableVersion(1, 3, 1).compatibleWith(stableVersion(1, 4, 0)), + "1.3.1 not compatible with 1.4.0"); + + // 0.y.z should not be compatible with any other version + assertFalse(stableVersion(0, 4, 0).compatibleWith(stableVersion(0, 4, 1)), + "0.4.0 compatible with 0.4.1 (0.y.z versions should be treated as unstable/incompatbile)"); + + // upgrading major version should = incompatible + assertFalse(stableVersion(1, 0, 0).compatibleWith(stableVersion(2, 0, 0)), + "1.0.0 compatible with 2.0.0"); + + // dowgrade should = incompatible + assertFalse(stableVersion(1, 1, 0).compatibleWith(stableVersion(1, 0, 0)), + "1.1.0 compatible with 1.0.0"); + } + + /** + * Tests {@link SemanticVersionNumber#toString} for complex version numbers + * + * @since 2022-02-19 + */ + @Test + public void testComplexToString() { + final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) + .build(); + assertEquals("1.2.3-1.2.3", v1.toString()); + final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) + .buildMetadata("2022-02-19").build(); + assertEquals("4.5.6-abc.123+2022-02-19", v2.toString()); + final SemanticVersionNumber v3 = builder(1, 0, 0) + .preRelease("x-y-z", "--").build(); + assertEquals("1.0.0-x-y-z.--", v3.toString()); + } + + /** + * Tests that complex version can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testComplexVersions() { + final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) + .build(); + assertEquals(1, v1.majorVersion()); + assertEquals(2, v1.minorVersion()); + assertEquals(3, v1.patchVersion()); + assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers()); + assertEquals(List.of(), v1.buildMetadata()); + + final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) + .buildMetadata("2022-02-19").build(); + assertEquals(4, v2.majorVersion()); + assertEquals(5, v2.minorVersion()); + assertEquals(6, v2.patchVersion()); + assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers()); + assertEquals(List.of("2022-02-19"), v2.buildMetadata()); + + final SemanticVersionNumber v3 = builder(1, 0, 0) + .preRelease("x-y-z", "--").build(); + assertEquals(1, v3.majorVersion()); + assertEquals(0, v3.minorVersion()); + assertEquals(0, v3.patchVersion()); + assertEquals(List.of("x-y-z", "--"), v3.preReleaseIdentifiers()); + assertEquals(List.of(), v3.buildMetadata()); + } + + /** + * Test that semantic version strings can be parsed correctly + * + * @since 2022-02-19 + * @see SemanticVersionNumber#fromString + * @see SemanticVersionNumber#isValidVersionString + */ + @Test + public void testFromString() { + // test that the regex can match version strings + assertTrue(isValidVersionString("1.0.0"), "1.0.0 is treated as invalid"); + assertTrue(isValidVersionString("1.3.9"), "1.3.9 is treated as invalid"); + assertTrue(isValidVersionString("2.0.0-a.1"), + "2.0.0-a.1 is treated as invalid"); + assertTrue(isValidVersionString("1.0.0-a.b.c.d"), + "1.0.0-a.b.c.d is treated as invalid"); + assertTrue(isValidVersionString("1.0.0+abc"), + "1.0.0+abc is treated as invalid"); + assertTrue(isValidVersionString("1.0.0-abc+def"), + "1.0.0-abc+def is treated as invalid"); + + // test that invalid versions don't match + assertFalse(isValidVersionString("1.0"), + "1.0 is treated as valid (patch should be required)"); + assertFalse(isValidVersionString("1.A.0"), + "1.A.0 is treated as valid (main versions must be numbers)"); + assertFalse(isValidVersionString("1.0.0-"), + "1.0.0- is treated as valid (pre-release must not be empty)"); + assertFalse(isValidVersionString("1.0.0+"), + "1.0.0+ is treated as valid (build metadata must not be empty)"); + + // test that versions can be parsed + assertEquals(stableVersion(1, 0, 0), fromString("1.0.0"), + "Could not parse 1.0.0"); + assertEquals( + builder(1, 2, 3).preRelease("abc", "56", "def") + .buildMetadata("2022abc99").build(), + fromString("1.2.3-abc.56.def+2022abc99"), + "Could not parse 1.2.3-abc.56.def+2022abc99"); + } + + /** + * Ensures it is impossible to create invalid version numbers + */ + @Test + public void testInvalidVersionNumbers() { + // stableVersion() + assertThrows(IllegalArgumentException.class, + () -> stableVersion(1, 0, -1), + "Negative patch tolerated by stableVersion"); + assertThrows(IllegalArgumentException.class, + () -> stableVersion(1, -2, 1), + "Negative minor version number tolerated by stableVersion"); + assertThrows(IllegalArgumentException.class, + () -> stableVersion(-3, 0, 7), + "Negative major version number tolerated by stableVersion"); + + // preRelease() + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, -1, "test", 2), + "Negative patch tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, -2, 1, "test", 2), + "Negative minor version number tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(-3, 0, 7, "test", 2), + "Negative major version number tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "test", -1), + "Negative pre release number tolerated by preRelease"); + assertThrows(NullPointerException.class, + () -> preRelease(1, 0, 0, null, 1), "Null tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "", 1), + "Empty string tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "abc+cde", 1), + "Invalid string tolerated by preRelease"); + + // builder() + assertThrows(IllegalArgumentException.class, () -> builder(1, 0, -1), + "Negative patch tolerated by builder"); + assertThrows(IllegalArgumentException.class, () -> builder(1, -2, 1), + "Negative minor version number tolerated by builder"); + assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7), + "Negative major version number tolerated by builder"); + + final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3); + // note: builder.buildMetadata(null) doesn't even compile lol + // builder.buildMetadata + assertThrows(NullPointerException.class, + () -> testBuilder.buildMetadata(null, "abc"), + "Null tolerated by builder.buildMetadata(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(""), + "Empty string tolerated by builder.buildMetadata(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata("c%4"), + "Invalid string tolerated by builder.buildMetadata(String...)"); + assertThrows(NullPointerException.class, + () -> testBuilder.buildMetadata(List.of("abc", null)), + "Null tolerated by builder.buildMetadata(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(List.of("")), + "Empty string tolerated by builder.buildMetadata(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(List.of("")), + "Invalid string tolerated by builder.buildMetadata(List)"); + + // builder.preRelease + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(null, "abc"), + "Null tolerated by builder.preRelease(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(""), + "Empty string tolerated by builder.preRelease(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("c%4"), + "Invalid string tolerated by builder.preRelease(String...)"); + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(List.of("abc", null)), + "Null tolerated by builder.preRelease(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(List.of("")), + "Empty string tolerated by builder.preRelease(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(List.of("")), + "Invalid string tolerated by builder.preRelease(List)"); + + // the overloadings that accept numeric arguments + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(-1), + "Negative number tolerated by builder.preRelease(int...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("abc", -1), + "Negative number tolerated by builder.preRelease(String, int)"); + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(null, 1), + "Null tolerated by builder.preRelease(String, int)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("", 1), + "Empty string tolerated by builder.preRelease(String, int)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("#$#c", 1), + "Invalid string tolerated by builder.preRelease(String, int)"); + + // ensure all these attempts didn't change the builder + assertEquals(builder(1, 2, 3), testBuilder, + "Attempts at making invalid version number succeeded despite throwing errors"); + } + + /** + * Test for {@link SemanticVersionNumber#isStable} + * + * @since 2022-02-19 + */ + @Test + public void testIsStable() { + assertTrue(stableVersion(1, 0, 0).isStable(), + "1.0.0 should be stable but is not"); + assertFalse(stableVersion(0, 1, 2).isStable(), + "0.1.2 should not be stable but is"); + assertFalse(preRelease(1, 2, 3, "alpha", 5).isStable(), + "1.2.3a5 should not be stable but is"); + assertTrue( + builder(9, 9, 99) + .buildMetadata("lots-of-metadata", "abc123", "2022").build() + .isStable(), + "9.9.99+lots-of-metadata.abc123.2022 should be stable but is not"); + } + + /** + * Tests that the versions are ordered by + * {@link SemanticVersionNumber#compareTo} according to official rules. Tests + * all of the versions compared in section 11 of the SemVer 2.0.0 document + * and some more. + * + * @since 2022-02-19 + */ + @Test + public void testOrder() { + final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha") + .build(); // 1.0.0-alpha + final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1 + final SemanticVersionNumber v100ab = builder(1, 0, 0) + .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta + final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta") + .build(); // 1.0.0-alpha + final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2 + final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11 + final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1 + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + final SemanticVersionNumber v100plus = builder(1, 0, 0) + .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah + final SemanticVersionNumber v200 = stableVersion(2, 0, 0); + final SemanticVersionNumber v201 = stableVersion(2, 0, 1); + final SemanticVersionNumber v210 = stableVersion(2, 1, 0); + final SemanticVersionNumber v211 = stableVersion(2, 1, 1); + final SemanticVersionNumber v300 = stableVersion(3, 0, 0); + + // test order of version numbers + assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1"); + assertTrue(v100a1.compareTo(v100ab) < 0, + "1.0.0-alpha.1 >= 1.0.0-alpha.beta"); + assertTrue(v100ab.compareTo(v100b) < 0, "1.0.0-alpha.beta >= 1.0.0-beta"); + assertTrue(v100b.compareTo(v100b2) < 0, "1.0.0-beta >= 1.0.0-beta.2"); + assertTrue(v100b2.compareTo(v100b11) < 0, + "1.0.0-beta.2 >= 1.0.0-beta.11"); + assertTrue(v100b11.compareTo(v100rc1) < 0, "1.0.0-beta.11 >= 1.0.0-rc.1"); + assertTrue(v100rc1.compareTo(v100) < 0, "1.0.0-rc.1 >= 1.0.0"); + assertTrue(v100.compareTo(v200) < 0, "1.0.0 >= 2.0.0"); + assertTrue(v200.compareTo(v201) < 0, "2.0.0 >= 2.0.1"); + assertTrue(v201.compareTo(v210) < 0, "2.0.1 >= 2.1.0"); + assertTrue(v210.compareTo(v211) < 0, "2.1.0 >= 2.1.1"); + assertTrue(v211.compareTo(v300) < 0, "2.1.1 >= 3.0.0"); + + // test symmetry - assume previous tests passed + assertTrue(v100a1.compareTo(v100a) > 0, "1.0.0-alpha.1 <= 1.0.0-alpha"); + assertTrue(v100.compareTo(v100rc1) > 0, "1.0.0 <= 1.0.0-rc.1"); + assertTrue(v300.compareTo(v211) > 0, "3.0.0 <= 2.1.1"); + + // test transitivity + assertTrue(v100a.compareTo(v100b11) < 0, "1.0.0-alpha >= 1.0.0-beta.11"); + assertTrue(v100b.compareTo(v200) < 0, "1.0.0-beta >= 2.0.0"); + assertTrue(v100.compareTo(v300) < 0, "1.0.0 >= 3.0.0"); + assertTrue(v100a.compareTo(v300) < 0, "1.0.0-alpha >= 3.0.0"); + + // test metadata is ignored + assertEquals(0, v100.compareTo(v100plus), "Build metadata not ignored"); + // test metadata is NOT ignored by alternative comparator + assertTrue(BUILD_METADATA_COMPARATOR.compare(v100, v100plus) > 0, + "Build metadata ignored by BUILD_METADATA_COMPARATOR"); + } + + /** + * Tests that simple stable versions can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testSimpleStableVersions() { + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + assertEquals(1, v100.majorVersion()); + assertEquals(0, v100.minorVersion()); + assertEquals(0, v100.patchVersion()); + + final SemanticVersionNumber v925 = stableVersion(9, 2, 5); + assertEquals(9, v925.majorVersion()); + assertEquals(2, v925.minorVersion()); + assertEquals(5, v925.patchVersion()); + } + + /** + * Tests that {@link SemanticVersionNumber#toString} works for simple version + * numbers + * + * @since 2022-02-19 + */ + @Test + public void testSimpleToString() { + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + assertEquals("1.0.0", v100.toString()); + + final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1); + assertEquals("8.4.5-alpha.1", v845a1.toString()); + } + + /** + * Tests that simple unstable versions can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testSimpleUnstableVersions() { + final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1); + assertEquals(3, v350a1.majorVersion(), + "Incorrect major version for v3.5.0a1"); + assertEquals(5, v350a1.minorVersion(), + "Incorrect minor version for v3.5.0a1"); + assertEquals(0, v350a1.patchVersion(), + "Incorrect patch version for v3.5.0a1"); + assertEquals(List.of("alpha", "1"), v350a1.preReleaseIdentifiers(), + "Incorrect pre-release identifiers for v3.5.0a1"); + } +} diff --git a/src/test/java/sevenUnitsGUI/package-info.java b/src/test/java/sevenUnitsGUI/package-info.java deleted file mode 100644 index 96bdbd9..0000000 --- a/src/test/java/sevenUnitsGUI/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (C) 2022 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/** - * Tests for the new 7Units GUI - * - * @author Adrien Hopkins - * @since 2022-01-29 - */ -package sevenUnitsGUI; \ No newline at end of file -- cgit v1.2.3