Skip to content

Commit

Permalink
issue 607: Rework unit-test for version
Browse files Browse the repository at this point in the history
- Increased the number of test cases to cover corner cases.
- Fixed minor issues found during refactoring.

Signed-off-by: Oleksandr Mordyk <[email protected]>
  • Loading branch information
omordyk committed Oct 31, 2024
1 parent 9e05b3f commit 2a3d1cf
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 110 deletions.
116 changes: 61 additions & 55 deletions src/main/scala/org/openhorizon/exchangeapi/utility/VersionRange.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ")")
}
}
}
55 changes: 0 additions & 55 deletions src/test/scala/org/openhorizon/exchangeapi/VersionSuite.scala

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 2a3d1cf

Please sign in to comment.