From 2a3d1cf3557b8a0c9bcd0ad4929a8db2346db0dc Mon Sep 17 00:00:00 2001 From: Oleksandr Mordyk Date: Thu, 31 Oct 2024 03:14:55 -0700 Subject: [PATCH] issue 607: Rework unit-test for version - Increased the number of test cases to cover corner cases. - Fixed minor issues found during refactoring. Signed-off-by: Oleksandr Mordyk --- .../exchangeapi/utility/VersionRange.scala | 116 +++++++++--------- .../exchangeapi/VersionSuite.scala | 55 --------- .../route/version/TestVersion.scala | 74 +++++++++++ .../route/version/TestVersionRange.scala | 63 ++++++++++ 4 files changed, 198 insertions(+), 110 deletions(-) delete mode 100644 src/test/scala/org/openhorizon/exchangeapi/VersionSuite.scala create mode 100644 src/test/scala/org/openhorizon/exchangeapi/route/version/TestVersion.scala create mode 100644 src/test/scala/org/openhorizon/exchangeapi/route/version/TestVersionRange.scala diff --git a/src/main/scala/org/openhorizon/exchangeapi/utility/VersionRange.scala b/src/main/scala/org/openhorizon/exchangeapi/utility/VersionRange.scala index 9222340d..4b03be8f 100644 --- a/src/main/scala/org/openhorizon/exchangeapi/utility/VersionRange.scala +++ b/src/main/scala/org/openhorizon/exchangeapi/utility/VersionRange.scala @@ -3,59 +3,65 @@ package org.openhorizon.exchangeapi.utility import scala.util.matching.Regex /** Parse an osgi version range string and define includes() to test if a Version is in a VersionRange */ -final case class VersionRange(range: String) { - /* The typical format of a range is like [1.2.3,4.5.6), where - The 1st version is the lower bound (floor), if not specified 0.0.0 is the default - The 2nd version is the upper bound (ceiling), if not specified infinity is the default - [ or ] means inclusive on that side of the range - ( or ) means *not* inclusive of the limit on that side of the range - The default for the left side is [, the default for the right side is ) - For more detail, see section 3.2.6 of the OSGi Core Specification: https://www.osgi.org/developer/downloads/ - */ - // split the lower and upper bounds - val (firstPart, secondPart) = range.trim().toLowerCase.split("""\s*,\s*""") match { - case Array(s) => (s, "infinity") - case Array(s1, s2) => (s1, s2) - case _ => ("x", "x") - } - // split the leading [ or ( from the version number - val R1: Regex = """([\[(]?)(\d.*)""".r - val (floorInclusive, floor) = firstPart match { - case "" => (true, Version("0.0.0")) - case R1(i, f) => ((i != "("), Version(f)) - case _ => (false, Version("x")) // Version("x") is just an invalid version object - } - // separate the version number from the trailing ] or ) - val R2: Regex = """(.*\d)([\])]?)""".r - val R3: Regex = """(infinity)([\])]?)""".r - val (ceiling, ceilingInclusive) = secondPart match { // case "" => (Version("infinity"), false) - case R2(c, i) => (Version(c), (i == "]")) - case R3(c, i) => (Version(c), (i == "]")) - case _ => (Version("x"), true) - } - - def isValid: Boolean = (floor.isValid && ceiling.isValid) - - def includes(version: Version): Boolean = { - if (floorInclusive) { - if (floor > version) return false - } else { - if (floor >= version) return false - } - if (ceilingInclusive) { - if (version > ceiling) return false - } else { - if (version >= ceiling) return false - } - true - } - - // If this range is a single version (e.g. [1.2.3,1.2.3] ) return that version, otherwise None - def singleVersion: Option[Version] = { - if (floor == ceiling) Option(floor) else None - } - - override def toString: String = { - (if (floorInclusive) "[" else "(") + floor + "," + ceiling + (if (ceilingInclusive) "]" else ")") + final case class VersionRange(range: String) { + /* The typical format of a range is like [1.2.3,4.5.6), where + The 1st version is the lower bound (floor), if not specified 0.0.0 is the default + The 2nd version is the upper bound (ceiling), if not specified infinity is the default + [ or ] means inclusive on that side of the range + ( or ) means *not* inclusive of the limit on that side of the range + The default for the left side is [, the default for the right side is ) + For more detail, see section 3.2.6 of the OSGi Core Specification: https://www.osgi.org/developer/downloads/ + */ + // split the lower and upper bounds + val (firstPart, secondPart) = range.trim().toLowerCase.split("""\s*,\s*""") match { + case Array(s) => (s, "infinity") + case Array(s1, s2) => (s1, s2) + case _ => ("x", "x") + } + // split the leading [ or ( from the version number + val R1: Regex = """([\[(]?)(\d.*)""".r + val (floorInclusive, floor) = firstPart match { + case "" => (true, Version("0.0.0")) + case R1(i, f) => ((i != "("), Version(f)) + case _ => (false, Version("x")) // Version("x") is just an invalid version object + } + // separate the version number from the trailing ] or ) + val R2: Regex = """(.*\d)([\])]?)""".r + val R3: Regex = """(infinity)([\])]?)""".r + val (ceiling, ceilingInclusive) = secondPart match { // case "" => (Version("infinity"), false) + case R2(c, i) => (Version(c), (i == "]")) + case R3(c, i) => (Version(c), (i == "]")) + case _ => (Version("x"), true) + } + + def isValid: Boolean = { + if (firstPart.trim.isEmpty || secondPart.trim.isEmpty || secondPart.trim.isEmpty) { + return false + } + + floor.isValid && ceiling.isValid + } + + def includes(version: Version): Boolean = { + if (floorInclusive) { + if (floor > version) return false + } else { + if (floor >= version) return false + } + if (ceilingInclusive) { + if (version > ceiling) return false + } else { + if (version >= ceiling) return false + } + true + } + + // If this range is a single version (e.g. [1.2.3,1.2.3] ) return that version, otherwise None + def singleVersion: Option[Version] = { + if (floor == ceiling) Option(floor) else None + } + + override def toString: String = { + (if (floorInclusive) "[" else "(") + floor + "," + ceiling + (if (ceilingInclusive) "]" else ")") + } } -} diff --git a/src/test/scala/org/openhorizon/exchangeapi/VersionSuite.scala b/src/test/scala/org/openhorizon/exchangeapi/VersionSuite.scala deleted file mode 100644 index 195345d3..00000000 --- a/src/test/scala/org/openhorizon/exchangeapi/VersionSuite.scala +++ /dev/null @@ -1,55 +0,0 @@ -package org.openhorizon.exchangeapi - -import org.scalatest.funsuite.AnyFunSuite -import org.junit.runner.RunWith -import org.scalatestplus.junit.JUnitRunner -import org.openhorizon.exchangeapi._ -import org.openhorizon.exchangeapi.utility.{Version, VersionRange} - -/** - * Tests for the Version and VersionRange case classes - */ -@RunWith(classOf[JUnitRunner]) -class VersionSuite extends AnyFunSuite { - test("Version tests") { - assert(Version("1.2.3").isValid) - assert(Version("infinity").isValid) - assert(Version("Infinity").isValid) - assert(Version("INFINITY").isValid) - assert(!Version("1.2.3.4").isValid) - assert(!Version("x").isValid) - assert(Version("1.2.3").toString === "1.2.3") - assert(Version("1.0.0") === Version("1")) - assert(Version("1.2.3") != Version("1.3.2")) - assert(Version("2.2.3") > Version("1.3.2")) - assert(!(Version("1.2.3") > Version("1.3.2"))) - assert(Version("infinity") > Version("1.3.2")) - assert(!(Version("1.2.3") > Version("INFINITY"))) - assert(Version("1.2.3") >= Version("1.2.3")) - assert(Version("1.3.3") >= Version("1.2.3")) - assert(!(Version("1.2.2") >= Version("1.2.3"))) - } - - test("VersionRange tests") { - assert(VersionRange("1").toString === "[1.0.0,infinity)") - assert(!VersionRange("1,x").isValid) - assert(VersionRange("1,infinity]").isValid) - assert(VersionRange("1,INFINITY]").isValid) - assert(VersionRange("1").isValid) - assert(Version("1.2") in VersionRange("1")) - assert(Version("1.2") notIn VersionRange(" 1, 1.1")) - assert(Version("1.2") in VersionRange("1.2")) - assert(Version("1.2") notIn VersionRange("(1.2")) - assert(Version("1.2.3") in VersionRange("1.2,1.2.3]")) - assert(Version("1.2.3") in VersionRange("1.2")) - assert(Version("1.2.3") in VersionRange("1.2,")) - assert(Version("1.2.3") notIn VersionRange("(1.2,1.2.3")) - assert(Version("1.2.3") notIn VersionRange("(1.2,1.2.3)")) - assert(Version("1.2.3") in VersionRange("1.2,infinity")) - assert(Version("1.2.3") in VersionRange("1.2,INFINITY")) - assert(Version("1.2.3") in VersionRange("1.2,1.4")) - assert(Version("1.2.3") in VersionRange("[1.0.0,2.0.0)")) - assert(Version("1.0.0") in VersionRange("[1.0.0,2.0.0)")) - assert(Version("2.0.0") notIn VersionRange("[1.0.0,2.0.0)")) - } -} \ No newline at end of file diff --git a/src/test/scala/org/openhorizon/exchangeapi/route/version/TestVersion.scala b/src/test/scala/org/openhorizon/exchangeapi/route/version/TestVersion.scala new file mode 100644 index 00000000..959af7c6 --- /dev/null +++ b/src/test/scala/org/openhorizon/exchangeapi/route/version/TestVersion.scala @@ -0,0 +1,74 @@ +//package org.openhorizon.exchangeapi.route.version + +package org.openhorizon.exchangeapi +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import org.junit.runner.RunWith +import org.scalatestplus.junit.JUnitRunner +import org.openhorizon.exchangeapi._ +import org.openhorizon.exchangeapi.utility.{Version, VersionRange} + +/** + * Tests for the Version + */ +@RunWith(classOf[JUnitRunner]) +class TestVersion extends AnyFunSuite with Matchers { + test("Version validity tests") { + // Valid versions + assert(Version("1.2.3").isValid) + assert(Version("1.0.0").isValid) + assert(Version("0.0.0").isValid) + assert(Version("infinity").isValid) + assert(Version("Infinity").isValid) + assert(Version("INFINITY").isValid) + + // Invalid versions + assert(!Version("1.2.3.4").isValid) // Too many segments + assert(!Version("x").isValid) // Non-numeric + assert(!Version("").isValid) // Empty string + assert(!Version("1.2.a").isValid) // Invalid character + assert(!Version("1..2").isValid) // Double dot + assert(!Version("1.2..3").isValid) // Double dot in the middle + assert(!Version("1.2.-3").isValid) // Negative number + assert(!Version("-1.2.3").isValid) // Negative number at start + assert(!Version("1.2.3-").isValid) // Hyphen at the end + } + + test("Version string representation") { + assert(Version("1.2.3").toString === "1.2.3") + assert(Version("infinity").toString === "infinity") + assert(Version("0.0.0").toString === "0.0.0") + } + + test("Version equality tests") { + assert(Version("1.0.0") === Version("1")) + assert(Version("1.2.3") === Version("1.2.3")) + assert(Version("1.2.3") != Version("1.3.2")) + assert(Version("0.0.0") === Version("0.0.0")) + } + + test("Version comparison tests") { + assert(Version("2.2.3") > Version("1.3.2")) + assert(Version("1.2.3") > Version("1.2.2")) + assert(Version("1.2.3") >= Version("1.2.3")) + assert(Version("1.3.3") >= Version("1.2.3")) + assert(!(Version("1.2.2") >= Version("1.2.3"))) + + assert(Version("infinity") > Version("1.3.2")) + assert(!(Version("1.2.3") > Version("INFINITY"))) + + // Testing with leading zeros + assert(Version("1.2.3") === Version("01.2.3")) + assert(Version("1.2.3") > Version("1.2.02")) + assert(Version("1.2.3") >= Version("1.02.3")) + } + + test("Edge cases and performance tests") { + // Check upper limits + assert(Version("999999999.999999999.999999999").isValid) + + // Check behavior with invalid but interesting formats + assert(Version("1.0.0").isValid) + assert(Version("1.0").isValid) + } +} \ No newline at end of file diff --git a/src/test/scala/org/openhorizon/exchangeapi/route/version/TestVersionRange.scala b/src/test/scala/org/openhorizon/exchangeapi/route/version/TestVersionRange.scala new file mode 100644 index 00000000..6e370448 --- /dev/null +++ b/src/test/scala/org/openhorizon/exchangeapi/route/version/TestVersionRange.scala @@ -0,0 +1,63 @@ +package org.openhorizon.exchangeapi +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import org.junit.runner.RunWith +import org.scalatestplus.junit.JUnitRunner +import org.openhorizon.exchangeapi._ +import org.openhorizon.exchangeapi.utility.{Version, VersionRange} + +/** + * Tests for the Version Range + */ +@RunWith(classOf[JUnitRunner]) +class TestVersionRange extends AnyFunSuite with Matchers { + test("VersionRange tests") { + // Basic string representation tests + assert(VersionRange("1").toString === "[1.0.0,infinity)") + assert(VersionRange("1,infinity]").toString === "[1.0.0,infinity]") + assert(VersionRange("1.2,2").toString === "[1.2.0,2.0.0)") + + // Validity tests + assert(!VersionRange("1,x").isValid) // Invalid due to non-numeric + assert(VersionRange("1,infinity]").isValid) // Valid range with infinity + assert(VersionRange("1,INFINITY]").isValid) // Case insensitivity + assert(VersionRange("1").isValid) // Single version as valid range + assert(VersionRange("1.0.0").isValid) // Valid single version + assert(VersionRange("1.2,2.0.0").isValid) // Valid range + + // Inclusion tests + assert(Version("1.2") in VersionRange("1")) // Included in range starting with 1 + assert(Version("1.2") notIn VersionRange("1, 1.1")) // Not included in this range + assert(Version("1.2") in VersionRange("1.2")) // Exact match + assert(Version("1.2") notIn VersionRange("(1.2")) // Not included due to exclusive start + assert(Version("1.2.3") in VersionRange("1.2,1.2.3]")) // Included in inclusive end range + assert(Version("1.2.3") in VersionRange("1.2")) // Included in single version range + assert(Version("1.2.3") in VersionRange("1.2,")) // Open-ended range + assert(Version("1.2.3") notIn VersionRange("(1.2,1.2.3")) // Exclusive lower bound + assert(Version("1.2.3") notIn VersionRange("(1.2,1.2.3)")) // Exclusive bounds + assert(Version("1.2.3") in VersionRange("1.2,infinity")) // Open-ended to infinity + assert(Version("1.2.3") in VersionRange("1.2,INFINITY")) // Case insensitivity for infinity + assert(Version("1.2.3") in VersionRange("1.2,1.4")) // Included in this range + assert(Version("1.2.3") in VersionRange("[1.0.0,2.0.0)")) // Within valid range + assert(Version("1.0.0") in VersionRange("[1.0.0,2.0.0)")) // Exact match + assert(Version("2.0.0") notIn VersionRange("[1.0.0,2.0.0)")) // Outside range + } + + test("Additional VersionRange edge cases") { + // Test for overlapping ranges + assert(Version("1.5") in VersionRange("1.0,1.6")) // In overlapping range + assert(Version("1.0") in VersionRange("[1.0,1.5)")) // Lower bound inclusive + assert(Version("1.5") in VersionRange("(1.0,2.0)")) // Upper bound exclusive + assert(Version("1.0") notIn VersionRange("(1.0,1.5)")) // Exclusive lower bound + + // Edge case with negative version numbers + assert(!VersionRange("-1.0,0").isValid) // Invalid range with negative version + } + + test("Invalid VersionRange formats") { + assert(!VersionRange("").isValid) // Empty string + assert(!VersionRange(",2").isValid) // Invalid start + assert(!VersionRange("1,2,3").isValid) // Too many elements + assert(!VersionRange("1.0,)2.0").isValid) // Unmatched parentheses and invalid character + } +}