/**
* Copyright (C) 2022, 2024, 2025 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
* @since v0.4.0
*/
public final class SemanticVersionTest {
/**
* Test for {@link SemanticVersionNumber#compatible}
*
* @since 2022-02-20
* @since v0.4.0
*/
@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
* @since v0.4.0
*/
@Test
public void testComplexToString() {
final var v1 = builder(1, 2, 3).preRelease(1, 2, 3).build();
assertEquals("1.2.3-1.2.3", v1.toString());
final var 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 var 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
* @since v0.4.0
*/
@Test
public void testComplexVersions() {
final var 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 var 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 var 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
* @since v0.4.0
* @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 var 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
* @since v0.4.0
*/
@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
* @since v0.4.0
*/
@Test
public void testOrder() {
final var v100a = builder(1, 0, 0).preRelease("alpha").build(); // 1.0.0-alpha
final var v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1
final var v100ab = builder(1, 0, 0).preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta
final var v100b = builder(1, 0, 0).preRelease("beta").build(); // 1.0.0-alpha
final var v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2
final var v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11
final var v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1
final var v100 = stableVersion(1, 0, 0);
final var v100plus = builder(1, 0, 0)
.buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah
final var v200 = stableVersion(2, 0, 0);
final var v201 = stableVersion(2, 0, 1);
final var v210 = stableVersion(2, 1, 0);
final var v211 = stableVersion(2, 1, 1);
final var 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
* @since v0.4.0
*/
@Test
public void testSimpleStableVersions() {
final var v100 = stableVersion(1, 0, 0);
assertEquals(1, v100.majorVersion());
assertEquals(0, v100.minorVersion());
assertEquals(0, v100.patchVersion());
final var 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
* @since v0.4.0
*/
@Test
public void testSimpleToString() {
final var v100 = stableVersion(1, 0, 0);
assertEquals("1.0.0", v100.toString());
final var 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
* @since v0.4.0
*/
@Test
public void testSimpleUnstableVersions() {
final var 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");
}
}